diff --git a/.clang-tidy b/.clang-tidy deleted file mode 100644 index cc55fefd25..0000000000 --- a/.clang-tidy +++ /dev/null @@ -1,19 +0,0 @@ ---- -Checks: ' -bugprone-*, --bugprone-integer-division, --bugprone-narrowing-conversions, -performance-*, -clang-analyzer-*, -misc-*, --misc-unused-parameters, -modernize-*, --modernize-avoid-c-arrays, --modernize-deprecated-headers, --modernize-use-auto, --modernize-use-using, --modernize-use-nullptr, --modernize-use-trailing-return-type, -' -CheckOptions: -... diff --git a/.dockerignore b/.dockerignore index cfc34e5848..631ea04840 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,27 +13,6 @@ *.o-* *.os *.os-* -*.so -*.a venv/ .venv/ - -notebooks -phone -massivemap -neos -installer -chffr/app2 -chffr/backend/env -selfdrive/nav -selfdrive/baseui -selfdrive/test/simulator2 -**/cache_data -xx/plus -xx/community -xx/projects -!xx/projects/eon_testing_master -!xx/projects/map3d -xx/ops -xx/junk diff --git a/.gitattributes b/.gitattributes index cc1605a132..41a7367d84 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,10 +7,12 @@ *.png filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text *.ttf filter=lfs diff=lfs merge=lfs -text +*.otf filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text -system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text +system/hardware/tici/updater_weston filter=lfs diff=lfs merge=lfs -text +system/hardware/tici/updater_magic filter=lfs diff=lfs merge=lfs -text third_party/**/*.a filter=lfs diff=lfs merge=lfs -text third_party/**/*.so filter=lfs diff=lfs merge=lfs -text third_party/**/*.so.* filter=lfs diff=lfs merge=lfs -text diff --git a/.github/labeler.yaml b/.github/labeler.yaml index 278ac58036..711f4597bd 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -16,7 +16,7 @@ simulation: ui: - changed-files: - - any-glob-to-all-files: '{selfdrive/ui/**,system/ui/**}' + - any-glob-to-all-files: '{selfdrive/assets/**,selfdrive/ui/**,system/ui/**}' tools: - changed-files: diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 3019ba9cba..6cd7a4687b 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -23,7 +23,7 @@ jobs: - uses: ./.github/workflows/setup-with-retry - name: Push badges run: | - ${{ env.RUN }} "scons -j$(nproc) && python3 selfdrive/ui/translations/create_badges.py" + ${{ env.RUN }} "python3 selfdrive/ui/translations/create_badges.py" rm .gitattributes diff --git a/.github/workflows/ci_weekly_run.yaml b/.github/workflows/ci_weekly_run.yaml index a2a8ab31a2..4b239f201c 100644 --- a/.github/workflows/ci_weekly_run.yaml +++ b/.github/workflows/ci_weekly_run.yaml @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true jobs: - selfdrive_tests: - uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master + tests: + uses: sunnypilot/sunnypilot/.github/workflows/tests.yaml@master with: run_number: ${{ inputs.run_number }} diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/raylib_ui_preview.yaml similarity index 80% rename from .github/workflows/ui_preview.yaml rename to .github/workflows/raylib_ui_preview.yaml index 334b289632..99651a231e 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/raylib_ui_preview.yaml @@ -1,4 +1,4 @@ -name: "ui preview" +name: "raylib ui preview" on: push: branches: @@ -8,14 +8,16 @@ on: branches: - 'master' paths: + - 'selfdrive/assets/**' - 'selfdrive/ui/**' + - 'system/ui/**' workflow_dispatch: env: - UI_JOB_NAME: "Create UI Report" + UI_JOB_NAME: "Create raylib 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 }} - BRANCH_NAME: "openpilot/pr-${{ github.event.number }}" + BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui" jobs: preview: @@ -52,7 +54,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} run_id: ${{ steps.get_run_id.outputs.run_id }} search_artifacts: true - name: report-1-${{ env.REPORT_NAME }} + name: raylib-report-1-${{ env.REPORT_NAME }} path: ${{ github.workspace }}/pr_ui - name: Getting master ui @@ -60,23 +62,23 @@ jobs: with: repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} - path: ${{ github.workspace }}/master_ui - ref: openpilot_master_ui + path: ${{ github.workspace }}/master_ui_raylib + ref: openpilot_master_ui_raylib - name: Saving new master ui if: github.ref == 'refs/heads/master' && github.event_name == 'push' - working-directory: ${{ github.workspace }}/master_ui + working-directory: ${{ github.workspace }}/master_ui_raylib run: | - git checkout --orphan=new_master_ui + git checkout --orphan=new_master_ui_raylib git rm -rf * - git branch -D openpilot_master_ui - git branch -m openpilot_master_ui + git branch -D openpilot_master_ui_raylib + git branch -m openpilot_master_ui_raylib git config user.name "GitHub Actions Bot" git config user.email "<>" mv ${{ github.workspace }}/pr_ui/*.png . git add . - git commit -m "screenshots for commit ${{ env.SHA }}" - git push origin openpilot_master_ui --force + git commit -m "raylib screenshots for commit ${{ env.SHA }}" + git push origin openpilot_master_ui_raylib --force - name: Finding diff if: github.event_name == 'pull_request_target' @@ -94,7 +96,7 @@ 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 + if [ ! -f "${{ github.workspace }}/master_ui_raylib/${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{cyan}\\text{NEW}}\$\$" @@ -106,12 +108,12 @@ jobs: DIFF="${DIFF}" 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 + elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui_raylib/${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 + composite mask.png ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png + convert -delay 100 ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif - mv ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png + mv ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png DIFF="${DIFF}
" DIFF="${DIFF}${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$" @@ -149,7 +151,7 @@ jobs: - name: Saving proposed ui if: github.event_name == 'pull_request_target' - working-directory: ${{ github.workspace }}/master_ui + working-directory: ${{ github.workspace }}/master_ui_raylib run: | git config user.name "GitHub Actions Bot" git config user.email "<>" @@ -157,7 +159,7 @@ jobs: git rm -rf * mv ${{ github.workspace }}/pr_ui/* . git add . - git commit -m "screenshots for PR #${{ github.event.number }}" + git commit -m "raylib screenshots for PR #${{ github.event.number }}" git push origin ${{ env.BRANCH_NAME }} --force - name: Comment Screenshots on PR @@ -165,9 +167,9 @@ jobs: uses: thollander/actions-comment-pull-request@v2 with: message: | - - ## UI Preview + + ## raylib UI Preview ${{ steps.find_diff.outputs.DIFF }} - comment_tag: run_id_screenshots + comment_tag: run_id_screenshots_raylib pr_number: ${{ github.event.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/tests.yaml similarity index 87% rename from .github/workflows/selfdrive_tests.yaml rename to .github/workflows/tests.yaml index 85b7c61e5a..aab16ffbbe 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,4 +1,4 @@ -name: selfdrive +name: tests on: push: @@ -14,7 +14,7 @@ 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: tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: @@ -195,8 +195,6 @@ jobs: # Pre-compile Python bytecode so each pytest worker doesn't need to $PYTEST --collect-only -m 'not slow' -qq && \ MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \ - ./selfdrive/ui/tests/create_test_translations.sh && \ - QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ chmod -R 777 /tmp/comma_download_cache" process_replay: @@ -257,7 +255,7 @@ jobs: (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} - if: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + if: false # FIXME: Started to timeout recently steps: - uses: actions/checkout@v4 with: @@ -274,38 +272,28 @@ jobs: source selfdrive/test/setup_vsound.sh && \ CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py" - create_ui_report: - # This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml - name: Create UI Report + create_raylib_ui_report: + name: Create raylib UI Report runs-on: ${{ (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} - if: false # FIXME: FrameReader is broken on CI runners steps: - uses: actions/checkout@v4 with: submodules: true - uses: ./.github/workflows/setup-with-retry - - name: caching frames - id: frames-cache - uses: actions/cache@v4 - with: - path: .ci_cache/comma_download_cache - key: ui_screenshots_test_${{ hashFiles('selfdrive/ui/tests/test_ui/run.py') }} - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc)" - - name: Create Test Report - timeout-minutes: ${{ ((steps.frames-cache.outputs.cache-hit == 'true') && 2 || 4) }} + - name: Create raylib UI Report run: > - ${{ env.RUN }} "PYTHONWARNINGS=ignore && - source selfdrive/test/setup_xvfb.sh && - CACHE_ROOT=/tmp/comma_download_cache python3 selfdrive/ui/tests/test_ui/run.py && - chmod -R 777 /tmp/comma_download_cache" - - name: Upload Test Report + ${{ env.RUN }} "PYTHONWARNINGS=ignore && + source selfdrive/test/setup_xvfb.sh && + python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py" + - name: Upload Raylib UI Report uses: actions/upload-artifact@v4 with: - name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} - path: selfdrive/ui/tests/test_ui/report_1/screenshots + name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + path: selfdrive/ui/tests/test_ui/raylib_report/screenshots diff --git a/.gitignore b/.gitignore index 00a0533d86..b06d620d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ venv/ .overlay_init .overlay_consistent .sconsign.dblite -model2.png a.out .hypothesis .cache/ @@ -37,29 +36,23 @@ a.out *.class *.pyxbldc *.vcd -*.qm +*.mo *_pyx.cpp +*.stats config.json clcache compile_commands.json compare_runtime*.html -persist selfdrive/pandad/pandad cereal/services.h cereal/gen cereal/messaging/bridge -selfdrive/mapd/default_speeds_by_region.json selfdrive/ui/translations/tmp -selfdrive/test/longitudinal_maneuvers/out selfdrive/car/tests/cars_dump system/camerad/camerad system/camerad/test/ae_gray_test -notebooks -hyperthneed -provisioning - .coverage* coverage.xml htmlcov @@ -76,6 +69,7 @@ sunnypilot/modeld*/thneed/compile sunnypilot/modeld*/models/*.thneed sunnypilot/modeld*/models/*.pkl +# openpilot log files *.bz2 *.zst diff --git a/.gitmodules b/.gitmodules index af2dea6495..b9f0336a0e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,7 +15,7 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/tinygrad/tinygrad.git + 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/Dockerfile.openpilot b/Dockerfile.openpilot index d85be77121..106a06e3a2 100644 --- a/Dockerfile.openpilot +++ b/Dockerfile.openpilot @@ -9,4 +9,6 @@ WORKDIR ${OPENPILOT_PATH} COPY . ${OPENPILOT_PATH}/ -RUN scons --cache-readonly -j$(nproc) +ENV UV_BIN="/home/batman/.local/bin/" +ENV PATH="$UV_BIN:$PATH" +RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc) diff --git a/Jenkinsfile b/Jenkinsfile index f3a63d3dec..73fa74c1cd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -167,7 +167,7 @@ node { env.GIT_COMMIT = checkout(scm).GIT_COMMIT def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging', - 'release-tici', 'testing-closet*', 'hotfix-*'] + 'release-tici', 'release-tizi', 'release-tizi-staging', 'testing-closet*', 'hotfix-*'] def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*') if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) { @@ -178,8 +178,8 @@ node { try { if (env.BRANCH_NAME == 'devel-staging') { - deviceStage("build release3-staging", "tizi-needs-can", [], [ - step("build release3-staging", "RELEASE_BRANCH=release3-staging $SOURCE_DIR/release/build_release.sh"), + deviceStage("build release-tizi-staging", "tizi-needs-can", [], [ + step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging $SOURCE_DIR/release/build_release.sh"), ]) } diff --git a/RELEASES.md b/RELEASES.md index 189aa7ad54..9ab60e6cb7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,13 +1,20 @@ +Version 0.10.2 (2025-11-23) +======================== + Version 0.10.1 (2025-09-08) ======================== -* New driving model +* New driving model #36276 * World Model: removed global localization inputs * World Model: 2x the number of parameters * World Model: trained on 4x the number of segments + * VAE Compression Model: new architecture and training objective * Driving Vision Model: trained on 4x the number of segments +* New Driver Monitoring model #36198 +* Acura TLX 2021 support thanks to MVL! * Honda City 2023 support thanks to vanillagorillaa and drFritz! * Honda N-Box 2018 support thanks to miettal! * Honda Odyssey 2021-25 support thanks to csouers and MVL! +* Honda Passport 2026 support thanks to vanillagorillaa and MVL! Version 0.10.0 (2025-08-05) ======================== diff --git a/SConstruct b/SConstruct index 12d879b350..48dd044709 100644 --- a/SConstruct +++ b/SConstruct @@ -3,176 +3,52 @@ import subprocess import sys import sysconfig import platform +import shlex import numpy as np import SCons.Errors SCons.Warnings.warningAsException(True) -# pending upstream fix - https://github.com/SCons/scons/issues/4461 -#SetOption('warn', 'all') - -TICI = os.path.isfile('/TICI') -AGNOS = TICI - Decider('MD5-timestamp') SetOption('num_jobs', max(1, int(os.cpu_count()/2))) -AddOption('--kaitai', - action='store_true', - help='Regenerate kaitai struct parsers') - -AddOption('--asan', - action='store_true', - help='turn on ASAN') - -AddOption('--ubsan', - action='store_true', - help='turn on UBSan') - -AddOption('--coverage', - action='store_true', - help='build with test coverage options') - -AddOption('--clazy', - action='store_true', - help='build with clazy') - -AddOption('--ccflags', - action='store', - type='string', - default='', - help='pass arbitrary flags over the command line') - -AddOption('--external-sconscript', - action='store', - metavar='FILE', - dest='external_sconscript', - help='add an external SConscript to the build') - -AddOption('--mutation', - action='store_true', - help='generate mutation-ready code') - +AddOption('--kaitai', action='store_true', help='Regenerate kaitai struct parsers') +AddOption('--asan', action='store_true', help='turn on ASAN') +AddOption('--ubsan', action='store_true', help='turn on UBSan') +AddOption('--mutation', action='store_true', help='generate mutation-ready code') +AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line') AddOption('--minimal', action='store_false', dest='extras', - default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS) + default=os.path.exists(File('#.gitattributes').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 -## - x86_64: linux pc x64 -## - Darwin: mac x64 or arm64 -real_arch = arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() +# Detect platform +arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() if platform.system() == "Darwin": arch = "Darwin" brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() -elif arch == "aarch64" and AGNOS: +elif arch == "aarch64" and os.path.isfile('/TICI'): arch = "larch64" -assert arch in ["larch64", "aarch64", "x86_64", "Darwin"] - -lenv = { - "PATH": os.environ['PATH'], - "PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath, - - "ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath, - "ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath, - "TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer" -} - -rpath = [] - -if arch == "larch64": - cpppath = [ - "#third_party/opencl/include", - ] - - libpath = [ - "/usr/local/lib", - "/system/vendor/lib64", - f"#third_party/acados/{arch}/lib", - ] - - libpath += [ - "#third_party/snpe/larch64", - "#third_party/libyuv/larch64/lib", - "/usr/lib/aarch64-linux-gnu" - ] - cflags = ["-DQCOM2", "-mcpu=cortex-a57"] - cxxflags = ["-DQCOM2", "-mcpu=cortex-a57"] - rpath += ["/usr/local/lib"] -else: - cflags = [] - cxxflags = [] - cpppath = [] - rpath += [] - - # MacOS - if arch == "Darwin": - libpath = [ - f"#third_party/libyuv/{arch}/lib", - f"#third_party/acados/{arch}/lib", - f"{brew_prefix}/lib", - f"{brew_prefix}/opt/openssl@3.0/lib", - "/System/Library/Frameworks/OpenGL.framework/Libraries", - ] - - cflags += ["-DGL_SILENCE_DEPRECATION"] - cxxflags += ["-DGL_SILENCE_DEPRECATION"] - cpppath += [ - f"{brew_prefix}/include", - f"{brew_prefix}/opt/openssl@3.0/include", - ] - # Linux - else: - libpath = [ - f"#third_party/acados/{arch}/lib", - f"#third_party/libyuv/{arch}/lib", - "/usr/lib", - "/usr/local/lib", - ] - - if arch == "x86_64": - libpath += [ - f"#third_party/snpe/{arch}" - ] - rpath += [ - Dir(f"#third_party/snpe/{arch}").abspath, - ] - -if GetOption('asan'): - ccflags = ["-fsanitize=address", "-fno-omit-frame-pointer"] - ldflags = ["-fsanitize=address"] -elif GetOption('ubsan'): - ccflags = ["-fsanitize=undefined"] - ldflags = ["-fsanitize=undefined"] -else: - ccflags = [] - ldflags = [] - -# no --as-needed on mac linker -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(' ') +assert arch in [ + "larch64", # linux tici arm64 + "aarch64", # linux pc arm64 + "x86_64", # linux pc x64 + "Darwin", # macOS arm64 (x86 not supported) +] env = Environment( - ENV=lenv, + ENV={ + "PATH": os.environ['PATH'], + "PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath, + "ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath, + "ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath, + "TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer" + }, + CC='clang', + CXX='clang++', CCFLAGS=[ "-g", "-fPIC", @@ -185,37 +61,32 @@ env = Environment( "-Wno-c99-designator", "-Wno-reorder-init-list", "-Wno-vla-cxx-extension", - ] + cflags + ccflags, - - CPPPATH=cpppath + [ + ], + CFLAGS=["-std=gnu11"], + CXXFLAGS=["-std=c++1z"], + CPPPATH=[ "#", + "#msgq", + "#third_party", + "#third_party/json11", + "#third_party/linux/include", "#third_party/acados/include", "#third_party/acados/include/blasfeo/include", "#third_party/acados/include/hpipm/include", "#third_party/catch2/include", "#third_party/libyuv/include", - "#third_party/json11", - "#third_party/linux/include", "#third_party/snpe/include", - "#third_party", - "#msgq", ], - - CC='clang', - CXX='clang++', - LINKFLAGS=ldflags, - - RPATH=rpath, - - CFLAGS=["-std=gnu11"] + cflags, - CXXFLAGS=["-std=c++1z"] + cxxflags, - LIBPATH=libpath + [ + LIBPATH=[ + "#common", "#msgq_repo", "#third_party", "#selfdrive/pandad", - "#common", "#rednose/helpers", + f"#third_party/libyuv/{arch}/lib", + f"#third_party/acados/{arch}/lib", ], + RPATH=[], CYTHONCFILESUFFIX=".cpp", COMPILATIONDB_USE_ABSPATH=True, REDNOSE_ROOT="#", @@ -223,30 +94,72 @@ env = Environment( toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], ) -if arch == "Darwin": - # RPATH is not supported on macOS, instead use the linker flags - darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]] - env["LINKFLAGS"] += darwin_rpath_link_flags +# Arch-specific flags and paths +if arch == "larch64": + env.Append(CPPPATH=["#third_party/opencl/include"]) + env.Append(LIBPATH=[ + "/usr/local/lib", + "/system/vendor/lib64", + "/usr/lib/aarch64-linux-gnu", + "#third_party/snpe/larch64", + ]) + arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"] + env.Append(CCFLAGS=arch_flags) + env.Append(CXXFLAGS=arch_flags) +elif arch == "Darwin": + env.Append(LIBPATH=[ + f"{brew_prefix}/lib", + f"{brew_prefix}/opt/openssl@3.0/lib", + f"{brew_prefix}/opt/llvm/lib/c++", + "/System/Library/Frameworks/OpenGL.framework/Libraries", + ]) + env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"]) + env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"]) + env.Append(CPPPATH=[ + f"{brew_prefix}/include", + f"{brew_prefix}/opt/openssl@3.0/include", + ]) +else: + env.Append(LIBPATH=[ + "/usr/lib", + "/usr/local/lib", + ]) -env.CompilationDatabase('compile_commands.json') + if arch == "x86_64": + env.Append(LIBPATH=[ + f"#third_party/snpe/{arch}" + ]) + env.Append(RPATH=[ + Dir(f"#third_party/snpe/{arch}").abspath, + ]) -# Setup cache dir -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) +# Sanitizers and extra CCFLAGS from CLI +if GetOption('asan'): + env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"]) + env.Append(LINKFLAGS=["-fsanitize=address"]) +elif GetOption('ubsan'): + env.Append(CCFLAGS=["-fsanitize=undefined"]) + env.Append(LINKFLAGS=["-fsanitize=undefined"]) +_extra_cc = shlex.split(GetOption('ccflags') or '') +if _extra_cc: + env.Append(CCFLAGS=_extra_cc) + +# no --as-needed on mac linker +if arch != "Darwin": + env.Append(LINKFLAGS=["-Wl,--as-needed", "-Wl,--no-undefined"]) + +# progress output node_interval = 5 node_count = 0 def progress_function(node): global node_count node_count += node_interval sys.stderr.write("progress: %d\n" % node_count) - if os.environ.get('SCONS_PROGRESS'): Progress(progress_function, interval=node_interval) -# Cython build environment +# ********** Cython build environment ********** py_include = sysconfig.get_paths()['include'] envCython = env.Clone() envCython["CPPPATH"] += [py_include, np.get_include()] @@ -255,84 +168,27 @@ envCython["CCFLAGS"].remove("-Werror") envCython["LIBS"] = [] if arch == "Darwin": - envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"] + darwin_rpath_link_flags + envCython["LINKFLAGS"] = env["LINKFLAGS"] + ["-bundle", "-undefined", "dynamic_lookup"] else: envCython["LINKFLAGS"] = ["-pthread", "-shared"] np_version = SCons.Script.Value(np.__version__) Export('envCython', 'np_version') -# Qt build environment -qt_env = env.Clone() -qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"] +Export('env', 'arch') -qt_libs = [] -if arch == "Darwin": - qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5" - qt_dirs = [ - os.path.join(qt_env['QTDIR'], "include"), - ] - qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules] - qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")] - qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"] - qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin")) -else: - qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip() - qt_install_headers = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip() +# Setup cache dir +cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache' +CacheDir(cache_dir) +Clean(["."], cache_dir) - qt_env['QTDIR'] = qt_install_prefix - qt_dirs = [ - f"{qt_install_headers}", - ] - - qt_gui_path = os.path.join(qt_install_headers, "QtGui") - qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))] - qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else [] - qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules] - - qt_libs = [f"Qt5{m}" for m in qt_modules] - if arch == "larch64": - qt_libs += ["GLESv2", "wayland-client"] - qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath) - elif arch != "Darwin": - qt_libs += ["GL"] -qt_env['QT3DIR'] = qt_env['QTDIR'] -qt_env.Tool('qt3') - -qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"] -qt_flags = [ - "-D_REENTRANT", - "-DQT_NO_DEBUG", - "-DQT_WIDGETS_LIB", - "-DQT_GUI_LIB", - "-DQT_CORE_LIB", - "-DQT_MESSAGELOGCONTEXT", -] -qt_env['CXXFLAGS'] += qt_flags -qt_env['LIBPATH'] += ['#selfdrive/ui', ] -qt_env['LIBS'] = qt_libs - -if GetOption("clazy"): - checks = [ - "level0", - "level1", - "no-range-loop", - "no-non-pod-global-static", - ] - qt_env['CXX'] = 'clazy' - qt_env['ENV']['CLAZY_IGNORE_DIRS'] = qt_dirs[0] - qt_env['ENV']['CLAZY_CHECKS'] = ','.join(checks) - -Export('env', 'qt_env', 'arch', 'real_arch') +# ********** start building stuff ********** # Build common module SConscript(['common/SConscript']) -Import('_common', '_gpucommon') - +Import('_common') common = [_common, 'json11', 'zmq'] -gpucommon = [_gpucommon] - -Export('common', 'gpucommon') +Export('common') # Build messaging (cereal + msgq + socketmaster + their dependencies) # Enable swaglog include in submodules @@ -375,6 +231,5 @@ if Dir('#tools/cabana/').exists() and GetOption('extras'): if arch != "larch64": SConscript(['tools/cabana/SConscript']) -external_sconscript = GetOption('external_sconscript') -if external_sconscript: - SConscript([external_sconscript]) + +env.CompilationDatabase('compile_commands.json') diff --git a/cereal/log.capnp b/cereal/log.capnp index 8925bca8ae..891af58a21 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -918,6 +918,8 @@ struct ControlsState @0x97ff69c53601abf1 { saturated @7 :Bool; actualLateralAccel @9 :Float32; desiredLateralAccel @10 :Float32; + desiredLateralJerk @11 :Float32; + version @12 :Int32; } struct LateralLQRState { @@ -2146,13 +2148,10 @@ struct Joystick { struct DriverStateV2 { frameId @0 :UInt32; modelExecutionTime @1 :Float32; - dspExecutionTimeDEPRECATED @2 :Float32; gpuExecutionTime @8 :Float32; rawPredictions @3 :Data; - poorVisionProb @4 :Float32; wheelOnRightProb @5 :Float32; - leftDriverData @6 :DriverData; rightDriverData @7 :DriverData; @@ -2167,10 +2166,13 @@ struct DriverStateV2 { leftBlinkProb @7 :Float32; rightBlinkProb @8 :Float32; sunglassesProb @9 :Float32; - occludedProb @10 :Float32; - readyProb @11 :List(Float32); notReadyProb @12 :List(Float32); + occludedProbDEPRECATED @10 :Float32; + readyProbDEPRECATED @11 :List(Float32); } + + dspExecutionTimeDEPRECATED @2 :Float32; + poorVisionProbDEPRECATED @4 :Float32; } struct DriverStateDEPRECATED @0xb83c6cc593ed0a00 { @@ -2222,6 +2224,7 @@ struct DriverMonitoringState @0xb83cda094a1da284 { hiStdCount @14 :UInt32; isActiveMode @16 :Bool; isRHD @4 :Bool; + uncertainCount @19 :UInt32; isPreviewDEPRECATED @15 :Bool; rhdCheckedDEPRECATED @5 :Bool; diff --git a/common/SConscript b/common/SConscript index 3cdb6fc5a2..c771ee78b7 100644 --- a/common/SConscript +++ b/common/SConscript @@ -4,18 +4,12 @@ common_libs = [ 'params.cc', 'swaglog.cc', 'util.cc', - 'watchdog.cc', - 'ratekeeper.cc' -] - -_common = env.Library('common', common_libs, LIBS="json11") - -files = [ + 'ratekeeper.cc', 'clutil.cc', ] -_gpucommon = env.Library('gpucommon', files) -Export('_common', '_gpucommon') +_common = env.Library('common', common_libs, LIBS="json11") +Export('_common') if GetOption('extras'): env.Program('tests/test_common', diff --git a/common/api/__init__.py b/common/api/__init__.py index 3e238ccc7a..8b261486ba 100644 --- a/common/api/__init__.py +++ b/common/api/__init__.py @@ -14,9 +14,13 @@ class Api: def post(self, *args, **kwargs): return self.service.post(*args, **kwargs) - def get_token(self, expiry_hours=1): - return self.service.get_token(expiry_hours) + def get_token(self, payload_extra=None, expiry_hours=1): + return self.service.get_token(payload_extra, expiry_hours) def api_get(endpoint, method='GET', timeout=None, access_token=None, **params): return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params) + + +def get_key_pair(): + return CommaConnectApi(None).get_key_pair() diff --git a/common/api/base.py b/common/api/base.py index d40b47d49d..682b266056 100644 --- a/common/api/base.py +++ b/common/api/base.py @@ -1,18 +1,22 @@ import jwt +import os import requests import unicodedata from datetime import datetime, timedelta, UTC from openpilot.system.hardware.hw import Paths from openpilot.system.version import get_version + # name : jwt signature algorithm +KEYS = {"id_rsa" : "RS256", + "id_ecdsa" : "ES256"} + 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() + self.jwt_algorithm, self.private_key, _ = self.get_key_pair() def get(self, *args, **kwargs): return self.request('GET', *args, **kwargs) @@ -23,7 +27,7 @@ class BaseApi: 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): + def _get_token(self, payload_extra=None, expiry_hours=1, **extra_payload): now = datetime.now(UTC).replace(tzinfo=None) payload = { 'identity': self.dongle_id, @@ -32,13 +36,15 @@ class BaseApi: 'exp': now + timedelta(hours=expiry_hours), **extra_payload } - token = jwt.encode(payload, self.private_key, algorithm='RS256') + if payload_extra is not None: + payload.update(payload_extra) + token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm) if isinstance(token, bytes): token = token.decode('utf8') return token - def get_token(self, expiry_hours=1): - return self._get_token(expiry_hours) + def get_token(self, payload_extra=None, expiry_hours=1): + return self._get_token(payload_extra, expiry_hours) def remove_non_ascii_chars(self, text): normalized_text = unicodedata.normalize('NFD', text) @@ -54,3 +60,11 @@ class BaseApi: headers['User-Agent'] = self.user_agent + version return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params) + + @staticmethod + def get_key_pair(): + for key in KEYS: + if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): + with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: + return KEYS[key], private.read(), public.read() + return None, None, None diff --git a/common/dict_helpers.py b/common/dict_helpers.py deleted file mode 100644 index 62cff63b58..0000000000 --- a/common/dict_helpers.py +++ /dev/null @@ -1,9 +0,0 @@ -# remove all keys that end in DEPRECATED -def strip_deprecated_keys(d): - for k in list(d.keys()): - if isinstance(k, str): - if k.endswith('DEPRECATED'): - d.pop(k) - elif isinstance(d[k], dict): - strip_deprecated_keys(d[k]) - return d diff --git a/common/git.py b/common/git.py index 4406bf96b1..2296fa7088 100644 --- a/common/git.py +++ b/common/git.py @@ -1,6 +1,6 @@ from functools import cache import subprocess -from openpilot.common.run import run_cmd, run_cmd_default +from openpilot.common.utils import run_cmd, run_cmd_default @cache diff --git a/common/model.h b/common/model.h index a1e034c06f..03773b633a 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "Firehose (Default)" +#define DEFAULT_MODEL "The Cool People (Default)" diff --git a/common/params_keys.h b/common/params_keys.h index df53261eb2..38578d7ddc 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -66,7 +66,7 @@ inline static std::unordered_map keys = { {"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}}, {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, - {"LanguageSetting", {PERSISTENT | BACKUP, STRING, "main_en"}}, + {"LanguageSetting", {PERSISTENT | BACKUP, STRING, "en"}}, {"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}}, {"LastGPSPosition", {PERSISTENT, STRING}}, {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, @@ -97,6 +97,7 @@ inline static std::unordered_map keys = { {"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_DriverMonitoringUncertain", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}}, {"OpenpilotEnabledToggle", {PERSISTENT | BACKUP, BOOL, "1"}}, {"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, @@ -108,6 +109,7 @@ inline static std::unordered_map keys = { {"RecordFront", {PERSISTENT | BACKUP, BOOL}}, {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet {"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}}, + {"ShowDebugInfo", {PERSISTENT, BOOL}}, {"RouteCount", {PERSISTENT, INT, "0"}}, {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"SshEnabled", {PERSISTENT | BACKUP, BOOL}}, diff --git a/common/pid.py b/common/pid.py index 99142280ca..e3fa8afdf4 100644 --- a/common/pid.py +++ b/common/pid.py @@ -2,11 +2,10 @@ import numpy as np from numbers import Number class PIDController: - def __init__(self, k_p, k_i, k_f=0., k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100): + def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100): self._k_p = k_p self._k_i = k_i self._k_d = k_d - self.k_f = k_f # feedforward gain if isinstance(self._k_p, Number): self._k_p = [[0], [self._k_p]] if isinstance(self._k_i, Number): @@ -16,7 +15,7 @@ class PIDController: self.set_limits(pos_limit, neg_limit) - self.i_rate = 1.0 / rate + self.i_dt = 1.0 / rate self.speed = 0.0 self.reset() @@ -46,12 +45,12 @@ class PIDController: def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False): self.speed = speed - self.p = float(error) * self.k_p - self.f = feedforward * self.k_f - self.d = error_rate * self.k_d + self.p = self.k_p * float(error) + self.d = self.k_d * error_rate + self.f = feedforward if not freeze_integrator: - i = self.i + error * self.k_i * self.i_rate + i = self.i + self.k_i * self.i_dt * error # Don't allow windup if already clipping test_control = self.p + i + self.d + self.f diff --git a/common/retry.py b/common/retry.py deleted file mode 100644 index 9bd4ac9522..0000000000 --- a/common/retry.py +++ /dev/null @@ -1,30 +0,0 @@ -import time -import functools - -from openpilot.common.swaglog import cloudlog - - -def retry(attempts=3, delay=1.0, ignore_failure=False): - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - for _ in range(attempts): - try: - return func(*args, **kwargs) - except Exception: - cloudlog.exception(f"{func.__name__} failed, trying again") - time.sleep(delay) - - if ignore_failure: - cloudlog.error(f"{func.__name__} failed after retry") - else: - raise Exception(f"{func.__name__} failed after retry") - return wrapper - return decorator - - -if __name__ == "__main__": - @retry(attempts=10) - def abc(): - raise ValueError("abc failed :(") - abc() diff --git a/common/run.py b/common/run.py deleted file mode 100644 index 75395ead1f..0000000000 --- a/common/run.py +++ /dev/null @@ -1,28 +0,0 @@ -import subprocess -from contextlib import contextmanager -from subprocess import Popen, PIPE, TimeoutExpired - - -def run_cmd(cmd: list[str], cwd=None, env=None) -> str: - return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip() - - -def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str: - try: - return run_cmd(cmd, cwd=cwd, env=env) - except subprocess.CalledProcessError: - return default - - -@contextmanager -def managed_proc(cmd: list[str], env: dict[str, str]): - proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE) - try: - yield proc - finally: - if proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=5) - except TimeoutExpired: - proc.kill() diff --git a/common/tests/test_file_helpers.py b/common/tests/test_file_helpers.py index a9977c2362..c7fe1984c5 100644 --- a/common/tests/test_file_helpers.py +++ b/common/tests/test_file_helpers.py @@ -1,7 +1,7 @@ import os from uuid import uuid4 -from openpilot.common.file_helpers import atomic_write_in_dir +from openpilot.common.utils import atomic_write_in_dir class TestFileHelpers: diff --git a/common/file_helpers.py b/common/utils.py similarity index 51% rename from common/file_helpers.py rename to common/utils.py index b0d889f163..89c0601f06 100644 --- a/common/file_helpers.py +++ b/common/utils.py @@ -2,9 +2,14 @@ import io import os import tempfile import contextlib +import subprocess +import time +import functools +from subprocess import Popen, PIPE, TimeoutExpired import zstandard as zstd +from openpilot.common.swaglog import cloudlog -LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change +LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change class CallbackReader: @@ -27,7 +32,7 @@ class CallbackReader: @contextlib.contextmanager -def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str = None, newline: str = None, +def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None, overwrite: bool = False): """Write to a file atomically using a temporary file in the same directory as the destination file.""" dir_name = os.path.dirname(path) @@ -56,3 +61,58 @@ def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.Buffered compressed_size = compressed_stream.tell() compressed_stream.seek(0) return compressed_stream, compressed_size + + +# remove all keys that end in DEPRECATED +def strip_deprecated_keys(d): + for k in list(d.keys()): + if isinstance(k, str): + if k.endswith('DEPRECATED'): + d.pop(k) + elif isinstance(d[k], dict): + strip_deprecated_keys(d[k]) + return d + + +def run_cmd(cmd: list[str], cwd=None, env=None) -> str: + return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip() + + +def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str: + try: + return run_cmd(cmd, cwd=cwd, env=env) + except subprocess.CalledProcessError: + return default + + +@contextlib.contextmanager +def managed_proc(cmd: list[str], env: dict[str, str]): + proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE) + try: + yield proc + finally: + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except TimeoutExpired: + proc.kill() + + +def retry(attempts=3, delay=1.0, ignore_failure=False): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + for _ in range(attempts): + try: + return func(*args, **kwargs) + except Exception: + cloudlog.exception(f"{func.__name__} failed, trying again") + time.sleep(delay) + + if ignore_failure: + cloudlog.error(f"{func.__name__} failed after retry") + else: + raise Exception(f"{func.__name__} failed after retry") + return wrapper + return decorator diff --git a/common/version.h b/common/version.h index 669f303211..ef20670781 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.10.1" +#define COMMA_VERSION "0.10.2" diff --git a/common/watchdog.cc b/common/watchdog.cc deleted file mode 100644 index 44e8c83e6d..0000000000 --- a/common/watchdog.cc +++ /dev/null @@ -1,12 +0,0 @@ -#include - -#include "common/watchdog.h" -#include "common/util.h" -#include "system/hardware/hw.h" - -const std::string watchdog_fn_prefix = Path::shm_path() + "/wd_"; // + - -bool watchdog_kick(uint64_t ts) { - static std::string fn = watchdog_fn_prefix + std::to_string(getpid()); - return util::write_file(fn.c_str(), &ts, sizeof(ts), O_WRONLY | O_CREAT) > 0; -} diff --git a/common/watchdog.h b/common/watchdog.h deleted file mode 100644 index 12dd2ca035..0000000000 --- a/common/watchdog.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -bool watchdog_kick(uint64_t ts); diff --git a/common/watchdog.py b/common/watchdog.py deleted file mode 100644 index ddb6f744e9..0000000000 --- a/common/watchdog.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -import time -import struct -from openpilot.system.hardware.hw import Paths - -WATCHDOG_FN = f"{Paths.shm_path()}/wd_" -_LAST_KICK = 0.0 - -def kick_watchdog(): - global _LAST_KICK - current_time = time.monotonic() - - if current_time - _LAST_KICK < 1.0: - return - - try: - with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f: - f.write(struct.pack('=2.0", @@ -72,7 +72,9 @@ dependencies = [ "zstandard", # ui + "raylib < 5.5.0.3", # TODO: unpin when they fix https://github.com/electronstudio/raylib-python-cffi/issues/186 "qrcode", + "mapbox-earcut", ] [project.optional-dependencies] @@ -119,7 +121,6 @@ dev = [ "tabulate", "types-requests", "types-tabulate", - "raylib", ] tools = [ @@ -177,7 +178,7 @@ quiet-level = 3 # if you've got a short variable name that's getting flagged, add it here ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite" builtin = "clear,rare,informal,code,names,en-GB_to_en-US" -skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.ts, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*" +skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*" [tool.mypy] python_version = "3.11" @@ -235,7 +236,6 @@ lint.ignore = [ "B027", "B024", "NPY002", # new numpy random syntax is worse - "UP038", # (x, y) -> x|y for isinstance ] line-length = 160 target-version ="py311" @@ -263,8 +263,13 @@ lint.flake8-implicit-str-concat.allow-multiline = false "tools".msg = "Use openpilot.tools" "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" "unittest".msg = "Use pytest" -"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" "time.time".msg = "Use time.monotonic" +# raylib banned APIs +"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" +"pyray.is_mouse_button_pressed".msg = "This can miss events. Use Widget._handle_mouse_press" +"pyray.is_mouse_button_released".msg = "This can miss events. Use Widget._handle_mouse_release" +"pyray.draw_text".msg = "Use a function (such as rl.draw_font_ex) that takes font as an argument" + [tool.ruff.format] quote-style = "preserve" diff --git a/release/pack.py b/release/pack.py index 1cb1a47a48..92ff68fe76 100755 --- a/release/pack.py +++ b/release/pack.py @@ -12,7 +12,7 @@ from openpilot.common.basedir import BASEDIR DIRS = ['cereal', 'openpilot'] -EXTS = ['.png', '.py', '.ttf', '.capnp'] +EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo'] INTERPRETER = '/usr/bin/env python3' diff --git a/selfdrive/SConscript b/selfdrive/SConscript index 0b49e69116..55f347c44e 100644 --- a/selfdrive/SConscript +++ b/selfdrive/SConscript @@ -3,4 +3,4 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript']) SConscript(['controls/lib/longitudinal_mpc_lib/SConscript']) SConscript(['locationd/SConscript']) SConscript(['modeld/SConscript']) -SConscript(['ui/SConscript']) \ No newline at end of file +SConscript(['ui/SConscript']) diff --git a/selfdrive/assets/.gitignore b/selfdrive/assets/.gitignore index 1f90a2a932..fffd4b4ed9 100644 --- a/selfdrive/assets/.gitignore +++ b/selfdrive/assets/.gitignore @@ -1,2 +1,4 @@ *.cc +fonts/*.fnt +fonts/*.png translations_assets.qrc diff --git a/selfdrive/assets/fonts/NotoColorEmoji.ttf b/selfdrive/assets/fonts/NotoColorEmoji.ttf new file mode 100644 index 0000000000..778e821ce3 --- /dev/null +++ b/selfdrive/assets/fonts/NotoColorEmoji.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93cdc4ee9aa40e2afceecc63da0ca05ec7aab4bec991ece51a6b52389f48a477 +size 10788068 diff --git a/selfdrive/assets/fonts/process.py b/selfdrive/assets/fonts/process.py new file mode 100755 index 0000000000..a0d01af148 --- /dev/null +++ b/selfdrive/assets/fonts/process.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +from pathlib import Path +import json + +import pyray as rl + +FONT_DIR = Path(__file__).resolve().parent +SELFDRIVE_DIR = FONT_DIR.parents[1] +TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations" +LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json" + +GLYPH_PADDING = 6 +EXTRA_CHARS = "–‑✓×°§•€£¥" +UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"} + + +def _languages(): + if not LANGUAGES_FILE.exists(): + return {} + with LANGUAGES_FILE.open(encoding="utf-8") as f: + return json.load(f) + + +def _char_sets(): + base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS) + unifont = set(base) + + for language, code in _languages().items(): + unifont.update(language) + po_path = TRANSLATIONS_DIR / f"app_{code}.po" + try: + chars = set(po_path.read_text(encoding="utf-8")) + except FileNotFoundError: + continue + (unifont if code in UNIFONT_LANGUAGES else base).update(chars) + + return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont)) + + +def _glyph_metrics(glyphs, rects, codepoints): + entries = [] + min_offset_y, max_extent = None, 0 + for idx, codepoint in enumerate(codepoints): + glyph = glyphs[idx] + rect = rects[idx] + width = int(round(rect.width)) + height = int(round(rect.height)) + offset_y = int(round(glyph.offsetY)) + min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y) + max_extent = max(max_extent, offset_y + height) + entries.append({ + "id": codepoint, + "x": int(round(rect.x)), + "y": int(round(rect.y)), + "width": width, + "height": height, + "xoffset": int(round(glyph.offsetX)), + "yoffset": offset_y, + "xadvance": int(round(glyph.advanceX)), + }) + + if min_offset_y is None: + raise RuntimeError("No glyphs were generated") + + line_height = int(round(max_extent - min_offset_y)) + base = int(round(max_extent)) + return entries, line_height, base + + +def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries): + lines = [ + f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0", + f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4", + f"page id=0 file=\"{atlas_name}\"", + f"chars count={len(entries)}", + ] + for entry in entries: + lines.append( + ("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " + + "xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry) + ) + path.write_text("\n".join(lines) + "\n") + + +def _process_font(font_path: Path, codepoints: tuple[int, ...]): + print(f"Processing {font_path.name}...") + + font_size = { + "unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph + }.get(font_path.name, 200) + + data = font_path.read_bytes() + file_buf = rl.ffi.new("unsigned char[]", data) + cp_buffer = rl.ffi.new("int[]", codepoints) + cp_ptr = rl.ffi.cast("int *", cp_buffer) + glyphs = rl.load_font_data(rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints), rl.FontType.FONT_DEFAULT) + if glyphs == rl.ffi.NULL: + raise RuntimeError("raylib failed to load font data") + + rects_ptr = rl.ffi.new("Rectangle **") + image = rl.gen_image_font_atlas(glyphs, rects_ptr, len(codepoints), font_size, GLYPH_PADDING, 0) + if image.width == 0 or image.height == 0: + raise RuntimeError("raylib returned an empty atlas") + + rects = rects_ptr[0] + atlas_name = f"{font_path.stem}.png" + atlas_path = FONT_DIR / atlas_name + entries, line_height, base = _glyph_metrics(glyphs, rects, codepoints) + + if not rl.export_image(image, atlas_path.as_posix()): + raise RuntimeError("Failed to export atlas image") + + _write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries) + + +def main(): + base_cp, unifont_cp = _char_sets() + fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf")) + for font in fonts: + if "emoji" in font.name.lower(): + continue + glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp + _process_font(font, glyphs) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/selfdrive/assets/fonts/unifont.otf b/selfdrive/assets/fonts/unifont.otf new file mode 100644 index 0000000000..b85597b1f4 --- /dev/null +++ b/selfdrive/assets/fonts/unifont.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9712a9bc089af7ddc06e0826aa84f2ee23ed2f1a1dddaf2a89c2483e753a8475 +size 5321484 diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 821767ee46..61a7906652 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -62,8 +62,8 @@ class TestCarInterfaces: # hypothesis also slows down significantly with just one more message draw LongControl(car_params, car_params_sp) if car_params.steerControlType == CarParams.SteerControlType.angle: - LatControlAngle(car_params, car_params_sp, car_interface) + LatControlAngle(car_params, car_params_sp, car_interface, DT_CTRL) elif car_params.lateralTuning.which() == 'pid': - LatControlPID(car_params, car_params_sp, car_interface) + LatControlPID(car_params, car_params_sp, car_interface, DT_CTRL) elif car_params.lateralTuning.which() == 'torque': - LatControlTorque(car_params, car_params_sp, car_interface) + LatControlTorque(car_params, car_params_sp, car_interface, DT_CTRL) diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 48da4b1ce2..a5a0b1ce6a 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -189,7 +189,7 @@ class TestCarModelBase(unittest.TestCase): if tuning == 'pid': self.assertTrue(len(self.CP.lateralTuning.pid.kpV)) elif tuning == 'torque': - self.assertTrue(self.CP.lateralTuning.torque.kf > 0) + self.assertTrue(self.CP.lateralTuning.torque.latAccelFactor > 0) else: raise Exception("unknown tuning") diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index b381879a7a..eaaf5a51e9 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -8,7 +8,7 @@ from cereal import car, log import cereal.messaging as messaging from openpilot.common.constants import CV from openpilot.common.params import Params -from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper +from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority, Ratekeeper from openpilot.common.swaglog import cloudlog from opendbc.car.car_helpers import interfaces @@ -19,6 +19,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl +from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose from openpilot.sunnypilot.livedelay.helpers import get_lat_delay @@ -45,7 +46,7 @@ class Controls(ControlsExt, ModelStateBase): self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP) - self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState', + self.sm = messaging.SubMaster(['liveDelay', 'liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState', 'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput', 'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext, poll='selfdriveState') @@ -62,11 +63,11 @@ class Controls(ControlsExt, ModelStateBase): self.VM = VehicleModel(self.CP) self.LaC: LatControl if self.CP.steerControlType == car.CarParams.SteerControlType.angle: - self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI) + self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'pid': - self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI) + self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'torque': - self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI) + self.LaC = LatControlTorque(self.CP, self.CP_SP, self.CI, DT_CTRL) def update(self): self.sm.update(15) @@ -139,11 +140,12 @@ class Controls(ControlsExt, ModelStateBase): # Reset desired curvature to current to avoid violating the limits on engage new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll) + lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS actuators.curvature = self.desired_curvature steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, self.steer_limited_by_safety, self.desired_curvature, - self.calibrated_pose, curvature_limited) # TODO what if not available + self.calibrated_pose, curvature_limited, lat_delay) actuators.torque = float(steer) actuators.steeringAngleDeg = float(steeringAngleDeg) # Ensure no NaNs/Infs diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index e28fa3021c..bf6dd04f60 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -22,7 +22,7 @@ def smooth_value(val, prev_val, tau, dt=DT_MDL): alpha = 1 - np.exp(-dt/tau) if tau > 0 else 1 return alpha * val + (1 - alpha) * prev_val -def clip_curvature(v_ego, prev_curvature, new_curvature, roll): +def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, bool]: # This function respects ISO lateral jerk and acceleration limits + a max curvature v_ego = max(v_ego, MIN_SPEED) max_curvature_rate = MAX_LATERAL_JERK / (v_ego ** 2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755 diff --git a/selfdrive/controls/lib/latcontrol.py b/selfdrive/controls/lib/latcontrol.py index 615168a262..4207a188f4 100644 --- a/selfdrive/controls/lib/latcontrol.py +++ b/selfdrive/controls/lib/latcontrol.py @@ -1,31 +1,31 @@ import numpy as np from abc import abstractmethod, ABC - -from openpilot.common.realtime import DT_CTRL +from openpilot.selfdrive.locationd.helpers import Pose class LatControl(ABC): - def __init__(self, CP, CP_SP, CI): - self.sat_count_rate = 1.0 * DT_CTRL + def __init__(self, CP, CP_SP, CI, dt): + self.dt = dt self.sat_limit = CP.steerLimitTimer - self.sat_count = 0. + self.sat_time = 0. self.sat_check_min_speed = 10. # we define the steer torque scale as [-1.0...1.0] self.steer_max = 1.0 @abstractmethod - def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose, + curvature_limited: bool, lat_delay: float): pass def reset(self): - self.sat_count = 0. + self.sat_time = 0. def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited): # Saturated only if control output is not being limited by car torque/angle rate limits if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed: - self.sat_count += self.sat_count_rate + self.sat_time += self.dt else: - self.sat_count -= self.sat_count_rate - self.sat_count = np.clip(self.sat_count, 0.0, self.sat_limit) - return self.sat_count > (self.sat_limit - 1e-3) + self.sat_time -= self.dt + self.sat_time = np.clip(self.sat_time, 0.0, self.sat_limit) + return self.sat_time > (self.sat_limit - 1e-3) diff --git a/selfdrive/controls/lib/latcontrol_angle.py b/selfdrive/controls/lib/latcontrol_angle.py index 787bd2dbe1..116d62f85c 100644 --- a/selfdrive/controls/lib/latcontrol_angle.py +++ b/selfdrive/controls/lib/latcontrol_angle.py @@ -8,12 +8,12 @@ STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees class LatControlAngle(LatControl): - def __init__(self, CP, CP_SP, CI): - super().__init__(CP, CP_SP, CI) + def __init__(self, CP, CP_SP, CI, dt): + super().__init__(CP, CP_SP, CI, dt) self.sat_check_min_speed = 5. self.use_steer_limited_by_safety = CP.brand == "tesla" - def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay): angle_log = log.ControlsState.LateralAngleState.new_message() if not active: diff --git a/selfdrive/controls/lib/latcontrol_pid.py b/selfdrive/controls/lib/latcontrol_pid.py index fd79c29bd1..25b2c8d87e 100644 --- a/selfdrive/controls/lib/latcontrol_pid.py +++ b/selfdrive/controls/lib/latcontrol_pid.py @@ -6,14 +6,15 @@ from openpilot.common.pid import PIDController class LatControlPID(LatControl): - def __init__(self, CP, CP_SP, CI): - super().__init__(CP, CP_SP, CI) + def __init__(self, CP, CP_SP, CI, dt): + super().__init__(CP, CP_SP, CI, dt) 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) + pos_limit=self.steer_max, neg_limit=-self.steer_max) + self.ff_factor = CP.lateralTuning.pid.kf self.get_steer_feedforward = CI.get_steer_feedforward_function() - def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay): pid_log = log.ControlsState.LateralPIDState.new_message() pid_log.steeringAngleDeg = float(CS.steeringAngleDeg) pid_log.steeringRateDeg = float(CS.steeringRateDeg) @@ -30,7 +31,7 @@ class LatControlPID(LatControl): else: # offset does not contribute to resistive torque - ff = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo) + ff = self.ff_factor * self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(error, diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 917fb233f4..47d105f993 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -1,9 +1,11 @@ import math import numpy as np +from collections import deque from cereal import log from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.common.pid import PIDController @@ -15,25 +17,34 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La # wheel slip, or to speed. # This controller applies torque to achieve desired lateral -# accelerations. To compensate for the low speed effects we -# use a LOW_SPEED_FACTOR in the error. Additionally, there is -# friction in the steering wheel that needs to be overcome to -# move it at all, this is compensated for too. +# accelerations. To compensate for the low speed effects the +# proportional gain is increased at low speeds by the PID controller. +# Additionally, there is friction in the steering wheel that needs +# to be overcome to move it at all, this is compensated for too. -LOW_SPEED_X = [0, 10, 20, 30] -LOW_SPEED_Y = [15, 13, 10, 5] +KP = 1.0 +KI = 0.3 +KD = 0.0 +INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] +KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] +LP_FILTER_CUTOFF_HZ = 1.2 +LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0 +VERSION = 0 class LatControlTorque(LatControl): - def __init__(self, CP, CP_SP, CI): - super().__init__(CP, CP_SP, CI) + def __init__(self, CP, CP_SP, CI, dt): + super().__init__(CP, CP_SP, CI, dt) self.torque_params = CP.lateralTuning.torque.as_builder() self.torque_from_lateral_accel = CI.torque_from_lateral_accel() self.lateral_accel_from_torque = CI.lateral_accel_from_torque() - self.pid = PIDController(self.torque_params.kp, self.torque_params.ki, - k_f=self.torque_params.kf) + self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt) self.update_limits() self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg + self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt) + self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len) + self.previous_measurement = 0.0 + self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) self.extension = LatControlTorqueExt(self, CP, CP_SP, CI) @@ -47,57 +58,68 @@ class LatControlTorque(LatControl): self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params), self.lateral_accel_from_torque(-self.steer_max, self.torque_params)) - def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay): # Override torque params from extension if self.extension.update_override_torque_params(self.torque_params): self.update_limits() pid_log = log.ControlsState.LateralTorqueState.new_message() + pid_log.version = VERSION if not active: output_torque = 0.0 pid_log.active = False else: - actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - - desired_lateral_accel = desired_curvature * CS.vEgo ** 2 - actual_lateral_accel = actual_curvature * CS.vEgo ** 2 lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 - low_speed_factor = np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)**2 - setpoint = desired_lateral_accel + low_speed_factor * desired_curvature - measurement = actual_lateral_accel + low_speed_factor * actual_curvature - gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation + delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len)) + expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] + # TODO factor out lateral jerk from error to later replace it with delay independent alternative + future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 + self.lat_accel_request_buffer.append(future_desired_lateral_accel) + gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation + desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay + + measurement = measured_curvature * CS.vEgo ** 2 + measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt) + self.previous_measurement = measurement + + setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel + error = setpoint - measurement # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly - pid_log.error = float(setpoint - measurement) - ff = gravity_adjusted_lateral_accel + pid_log.error = float(error) + ff = gravity_adjusted_future_lateral_accel # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll ff -= self.torque_params.latAccelOffset - ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + # TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it + ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 output_lataccel = self.pid.update(pid_log.error, - feedforward=ff, - speed=CS.vEgo, - freeze_integrator=freeze_integrator) + -measurement_rate, + feedforward=ff, + speed=CS.vEgo, + freeze_integrator=freeze_integrator) output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) # Lateral acceleration torque controller extension updates # Overrides pid_log.error and output_torque pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, - desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel, - desired_curvature, actual_curvature, steer_limited_by_safety, output_torque) + future_desired_lateral_accel, measurement, lateral_accel_deadzone, gravity_adjusted_future_lateral_accel, + desired_curvature, measured_curvature, steer_limited_by_safety, output_torque) pid_log.active = True pid_log.p = float(self.pid.p) pid_log.i = float(self.pid.i) pid_log.d = float(self.pid.d) pid_log.f = float(self.pid.f) - pid_log.output = float(-output_torque) # TODO: log lat accel? - pid_log.actualLateralAccel = float(actual_lateral_accel) - pid_log.desiredLateralAccel = float(desired_lateral_accel) + pid_log.output = float(-output_torque) # TODO: log lat accel? + pid_log.actualLateralAccel = float(measurement) + pid_log.desiredLateralAccel = float(setpoint) + pid_log.desiredLateralJerk = float(desired_lateral_jerk) pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited)) # TODO left is positive in this convention diff --git a/selfdrive/controls/lib/longcontrol.py b/selfdrive/controls/lib/longcontrol.py index b8b00e2fb9..ec714f452e 100644 --- a/selfdrive/controls/lib/longcontrol.py +++ b/selfdrive/controls/lib/longcontrol.py @@ -54,7 +54,7 @@ class LongControl: self.long_control_state = LongCtrlState.off self.pid = PIDController((CP.longitudinalTuning.kpBP, CP.longitudinalTuning.kpV), (CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV), - k_f=CP.longitudinalTuning.kf, rate=1 / DT_CTRL) + rate=1 / DT_CTRL) self.last_output_accel = 0.0 def reset(self): diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index 7906a31528..4180047617 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -7,6 +7,7 @@ from opendbc.car.toyota.values import CAR as TOYOTA from opendbc.car.nissan.values import CAR as NISSAN from opendbc.car.gm.values import CAR as GM from opendbc.car.vehicle_model import VehicleModel +from openpilot.common.realtime import DT_CTRL 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 @@ -29,7 +30,7 @@ class TestLatControl: CP_SP = convert_to_capnp(CP_SP) VM = VehicleModel(CP) - controller = controller(CP.as_reader(), CP_SP.as_reader(), CI) + controller = controller(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL) CS = car.CarState.new_message() CS.vEgo = 30 @@ -42,13 +43,13 @@ class TestLatControl: # Saturate for curvature limited and controller limited for _ in range(1000): - _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True) + _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True, 0.2) assert lac_log.saturated for _ in range(1000): - _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False) + _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False, 0.2) assert not lac_log.saturated for _ in range(1000): - _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False) + _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False, 0.2) assert lac_log.saturated diff --git a/selfdrive/debug/qlog_size.py b/selfdrive/debug/qlog_size.py index 6d494b6f75..2b54cfeebf 100755 --- a/selfdrive/debug/qlog_size.py +++ b/selfdrive/debug/qlog_size.py @@ -6,7 +6,7 @@ from collections import defaultdict import matplotlib.pyplot as plt from cereal.services import SERVICE_LIST -from openpilot.common.file_helpers import LOG_COMPRESSION_LEVEL +from openpilot.common.utils import LOG_COMPRESSION_LEVEL from openpilot.tools.lib.logreader import LogReader from tqdm import tqdm diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 57d53fdee3..835e1971c4 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import os import numpy as np from collections import deque, defaultdict @@ -250,6 +251,8 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt): def main(demo=False): config_realtime_process([0, 1, 2, 3], 5) + DEBUG = bool(int(os.getenv("DEBUG", "0"))) + pm = messaging.PubMaster(['liveTorqueParameters']) sm = messaging.SubMaster(['carControl', 'carOutput', 'carState', 'liveCalibration', 'livePose', 'liveDelay'], poll='livePose') @@ -268,7 +271,7 @@ def main(demo=False): # 4Hz driven by livePose if sm.frame % 5 == 0: - pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks())) + pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks(), with_points=DEBUG)) # Cache points every 60 seconds while onroad if sm.frame % 240 == 0: diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 1a6df4bb9b..f20855c2cb 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -1,11 +1,11 @@ import os import glob -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') +Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations') lenv = env.Clone() lenvCython = envCython.Clone() -libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] +libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread'] frameworks = [] common_src = [ diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 5aeb035bdc..2851a3e7da 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -25,13 +25,13 @@ from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE CALIB_LEN = 3 FEATURE_LEN = 512 -OUTPUT_SIZE = 84 + FEATURE_LEN +OUTPUT_SIZE = 83 + FEATURE_LEN PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl' - +# TODO: slice from meta class DriverStateResult(ctypes.Structure): _fields_ = [ ("face_orientation", ctypes.c_float*3), @@ -46,8 +46,8 @@ class DriverStateResult(ctypes.Structure): ("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), + ("_unused_c", ctypes.c_float), + ("_unused_d", ctypes.c_float*4), ("not_ready_prob", ctypes.c_float*2)] @@ -55,7 +55,6 @@ 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)] @@ -107,8 +106,6 @@ def fill_driver_state(msg, ds_result: DriverStateResult): 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] @@ -119,7 +116,6 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts: 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) diff --git a/selfdrive/modeld/models/README.md b/selfdrive/modeld/models/README.md index 255f28d80e..04b69c61c3 100644 --- a/selfdrive/modeld/models/README.md +++ b/selfdrive/modeld/models/README.md @@ -62,6 +62,5 @@ Refer to **slice_outputs** and **parse_vision_outputs/parse_policy_outputs** in * (deprecated) distracted probabilities: 2 * using phone probability: 1 * distracted probability: 1 - * common outputs 2 - * poor camera vision probability: 1 + * common outputs 1 * left hand drive probability: 1 diff --git a/selfdrive/modeld/models/dmonitoring_model.current b/selfdrive/modeld/models/dmonitoring_model.current deleted file mode 100644 index 121871ef2b..0000000000 --- a/selfdrive/modeld/models/dmonitoring_model.current +++ /dev/null @@ -1,2 +0,0 @@ -fa69be01-b430-4504-9d72-7dcb058eb6dd -d9fb22d1c4fa3ca3d201dbc8edf1d0f0918e53e6 diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index dcc727510e..1b6a8c3e93 100644 --- a/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50efe6451a3fb3fa04b6bb0e846544533329bd46ecefe9e657e91214dee2aaeb -size 7196502 +oid sha256:3a53626ab84757813fb16a1441704f2ae7192bef88c331bdc2415be6981d204f +size 7191776 diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 7b87846748..1e764af9ba 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebb38a934d6472c061cc6010f46d9720ca132d631a47e585a893bdd41ade2419 -size 12343535 +oid sha256:c5a1f0655ddf266ed42ad1980389d96f47cc5e756da1fa3ca1477a920bb9b157 +size 13926324 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 4b4fa05df8..441c4a16af 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:befac016a247b7ad5dc5b55d339d127774ed7bd2b848f1583f72aa4caee37781 -size 46271991 +oid sha256:8f16d548ea4eb5d01518a9e90d4527cd97c31a84bcaf6f695dead8f0015fecc4 +size 46271942 diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 9649df9a2b..d1630f0d5b 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -4,11 +4,13 @@ import numpy as np from cereal import car, log import cereal.messaging as messaging from openpilot.selfdrive.selfdrived.events import Events +from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.realtime import DT_DMON from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.stat_live import RunningStatFilter from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.system.hardware import HARDWARE EventName = log.OnroadEvent.EventName @@ -34,12 +36,13 @@ class DRIVER_MONITOR_SETTINGS: self._SG_THRESHOLD = 0.9 self._BLINK_THRESHOLD = 0.865 - self._EE_THRESH11 = 0.4 + if HARDWARE.get_device_type() == 'mici': + self._EE_THRESH11 = 0.75 + else: + self._EE_THRESH11 = 0.4 self._EE_THRESH12 = 15.0 self._EE_MAX_OFFSET1 = 0.06 self._EE_MIN_OFFSET1 = 0.025 - self._EE_THRESH21 = 0.01 - self._EE_THRESH22 = 0.35 self._POSE_PITCH_THRESHOLD = 0.3133 self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 @@ -55,6 +58,9 @@ class DRIVER_MONITOR_SETTINGS: self._YAW_MAX_OFFSET = 0.289 self._YAW_MIN_OFFSET = -0.0246 + self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1 + self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / self._DT_DMON) + self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / self._DT_DMON) self._POSESTD_THRESHOLD = 0.3 self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz @@ -137,11 +143,8 @@ class DriverMonitoring: self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) self.blink = DriverBlink() self.eev1 = 0. - self.eev2 = 1. self.ee1_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT) - self.ee2_offseter = RunningStatFilter(max_trackable=self.settings._POSE_OFFSET_MAX_COUNT) self.ee1_calibrated = False - self.ee2_calibrated = False self.always_on = always_on self.distracted_types = [] @@ -159,6 +162,9 @@ class DriverMonitoring: self.hi_stds = 0 self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME + self.dcam_uncertain_cnt = 0 + self.dcam_uncertain_alerted = False # once per drive + self.dcam_reset_cnt = 0 self.params = Params() self.too_distracted = self.params.get_bool("DriverTooDistracted") @@ -246,7 +252,7 @@ class DriverMonitoring: return distracted_types - def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged): + def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill): rhd_pred = driver_state.wheelOnRightProb # calibrates only when there's movement and either face detected if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or @@ -262,7 +268,7 @@ class DriverMonitoring: driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition, driver_data.faceOrientationStd, driver_data.facePositionStd, - driver_data.readyProb, driver_data.notReadyProb)): + driver_data.notReadyProb)): return self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD @@ -279,7 +285,6 @@ class DriverMonitoring: self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \ * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) self.eev1 = driver_data.notReadyProb[0] - self.eev2 = driver_data.readyProb[0] self.distracted_types = self._get_distracted_types() self.driver_distracted = (DistractedType.DISTRACTED_E2E in self.distracted_types or DistractedType.DISTRACTED_POSE in self.distracted_types @@ -293,12 +298,20 @@ class DriverMonitoring: self.pose.pitch_offseter.push_and_update(self.pose.pitch) self.pose.yaw_offseter.push_and_update(self.pose.yaw) self.ee1_offseter.push_and_update(self.eev1) - self.ee2_offseter.push_and_update(self.eev2) self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \ self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT self.ee1_calibrated = self.ee1_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT - self.ee2_calibrated = self.ee2_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT + + if self.face_detected and not self.driver_distracted: + if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD: + if not standstill: + self.dcam_uncertain_cnt += 1 + self.dcam_reset_cnt = 0 + else: + self.dcam_reset_cnt += 1 + if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT: + self.dcam_uncertain_cnt = 0 self.is_model_uncertain = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME self._set_timers(self.face_detected and not self.is_model_uncertain) @@ -376,6 +389,10 @@ class DriverMonitoring: if alert is not None: self.current_events.add(alert) + if self.dcam_uncertain_cnt > self.settings._DCAM_UNCERTAIN_ALERT_COUNT and not self.dcam_uncertain_alerted: + set_offroad_alert("Offroad_DriverMonitoringUncertain", True) + self.dcam_uncertain_alerted = True + def get_state_packet(self, valid=True): # build driverMonitoringState packet @@ -397,6 +414,7 @@ class DriverMonitoring: "hiStdCount": self.hi_stds, "isActiveMode": self.active_monitoring_mode, "isRHD": self.wheel_on_right, + "uncertainCount": self.dcam_uncertain_cnt, } return dat @@ -412,7 +430,8 @@ class DriverMonitoring: driver_state=sm['driverStateV2'], cal_rpy=sm['liveCalibration'].rpyCalib, car_speed=sm['carState'].vEgo, - op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive + op_engaged=sm['selfdriveState'].enabled or sm['carControl'].latActive, + standstill=sm['carState'].standstill, ) # Update distraction events diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py index 2a20b20dc1..1f8babe029 100644 --- a/selfdrive/monitoring/test_monitoring.py +++ b/selfdrive/monitoring/test_monitoring.py @@ -25,7 +25,6 @@ def make_msg(face_detected, distracted=False, model_uncertain=False): ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] # TODO: test both separately when e2e is used - ds.leftDriverData.readyProb = [0., 0., 0., 0.] ds.leftDriverData.notReadyProb = [0., 0.] return ds @@ -54,7 +53,7 @@ class TestMonitoring: DM = DriverMonitoring() events = [] for idx in range(len(msgs)): - DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx]) + DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx]) # cal_rpy and car_speed don't matter here # evaluate events at 10Hz for tests diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index bcb39391c4..55d033222b 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -80,7 +80,7 @@ Panda *connect(std::string serial="", uint32_t index=0) { } //panda->enable_deepsleep(); - for (int i = 0; i < PANDA_BUS_CNT; i++) { + for (int i = 0; i < PANDA_CAN_CNT; i++) { panda->set_can_fd_auto(i, true); } diff --git a/selfdrive/pandad/tests/test_pandad.py b/selfdrive/pandad/tests/test_pandad.py index 6a7359fd85..88d3939a6a 100644 --- a/selfdrive/pandad/tests/test_pandad.py +++ b/selfdrive/pandad/tests/test_pandad.py @@ -5,8 +5,7 @@ import time import cereal.messaging as messaging from cereal import log from openpilot.common.gpio import gpio_set, gpio_init -from panda import Panda, PandaDFU, PandaProtocolMismatch -from openpilot.common.retry import retry +from panda import Panda, PandaDFU from openpilot.system.manager.process_config import managed_processes from openpilot.system.hardware import HARDWARE from openpilot.system.hardware.tici.pins import GPIO @@ -50,8 +49,7 @@ class TestPandad: assert not Panda.wait_for_dfu(None, 3) assert not Panda.wait_for_panda(None, 3) - @retry(attempts=3) - def _flash_bootstub_and_test(self, fn, expect_mismatch=False): + def _flash_bootstub(self, fn): self._go_to_dfu() pd = PandaDFU(None) if fn is None: @@ -61,16 +59,6 @@ class TestPandad: pd.reset() HARDWARE.reset_internal_panda() - assert Panda.wait_for_panda(None, 10) - if expect_mismatch: - with pytest.raises(PandaProtocolMismatch): - Panda() - else: - with Panda() as p: - assert p.bootstub - - self._run_test(45) - def test_in_dfu(self): HARDWARE.recover_internal_panda() self._run_test(60) @@ -106,13 +94,14 @@ class TestPandad: print("startup times", ts, sum(ts) / len(ts)) assert 0.1 < (sum(ts)/len(ts)) < 0.7 - def test_protocol_version_check(self): - # flash old fw - fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin") - self._flash_bootstub_and_test(fn, expect_mismatch=True) + def test_old_spi_protocol(self): + # flash firmware with old SPI protocol + self._flash_bootstub(os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")) + self._run_test(45) def test_release_to_devel_bootstub(self): - self._flash_bootstub_and_test(None) + self._flash_bootstub(None) + self._run_test(45) def test_recover_from_bad_bootstub(self): self._go_to_dfu() diff --git a/selfdrive/pandad/tests/test_pandad_loopback.py b/selfdrive/pandad/tests/test_pandad_loopback.py index bf1c557128..eff70d2544 100644 --- a/selfdrive/pandad/tests/test_pandad_loopback.py +++ b/selfdrive/pandad/tests/test_pandad_loopback.py @@ -9,7 +9,7 @@ from pprint import pprint import cereal.messaging as messaging from cereal import car, log from opendbc.car.can_definitions import CanData -from openpilot.common.retry import retry +from openpilot.common.utils import retry from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.selfdrive.pandad import can_list_to_can_capnp diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 6bae034766..14a833a3ba 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -41,6 +41,10 @@ "text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1", "severity": 0 }, + "Offroad_DriverMonitoringUncertain": { + "text": "openpilot detected poor visibility for driver monitoring. Ensure the device has a clear view of the driver. This can be checked using Settings -> Device -> Driver Camera Preview. Extreme lighting conditions and/or unconventional mounting positions may also trigger this alert.", + "severity": 0 + }, "Offroad_ExcessiveActuation": { "text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.", "severity": 1, diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index b03a953a65..79a6aacb57 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -80,7 +80,7 @@ def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging. def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: return Alert( - f"Steer Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}", + f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}", "", AlertStatus.userPrompt, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4) @@ -322,7 +322,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { EventName.steerTempUnavailableSilent: { ET.WARNING: Alert( - "Steering Temporarily Unavailable", + "Steering Assist Temporarily Unavailable", "", AlertStatus.userPrompt, AlertSize.small, Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8), @@ -568,7 +568,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { }, EventName.steerTempUnavailable: { - ET.SOFT_DISABLE: soft_disable_alert("Steering Temporarily Unavailable"), + ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"), ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"), }, diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index a833fadb94..cdd4301fcf 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -afcab1abb62b9d5678342956cced4712f44e909e \ No newline at end of file +b508f43fb0481bce0859c9b6ab4f45ee690b8dab \ No newline at end of file diff --git a/selfdrive/test/setup_device_ci.sh b/selfdrive/test/setup_device_ci.sh index 98909bfb52..2a1442a20c 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/selfdrive/test/setup_device_ci.sh @@ -42,6 +42,7 @@ sudo systemctl restart NetworkManager sudo systemctl disable ssh-param-watcher.path sudo systemctl disable ssh-param-watcher.service sudo mount -o ro,remount / +sudo systemctl stop power_monitor while true; do if ! sudo systemctl is-active -q ssh; then @@ -54,7 +55,6 @@ while true; do # /data/ciui.py & #fi - awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog sleep 5s done diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index b4b9b9dbbe..69d920c1a0 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -32,7 +32,7 @@ CPU usage budget TEST_DURATION = 25 LOG_OFFSET = 8 -MAX_TOTAL_CPU = 300. # total for all 8 cores +MAX_TOTAL_CPU = 350. # total for all 8 cores PROCS = { # Baseline CPU usage by process "selfdrive.controls.controlsd": 16.0, @@ -42,7 +42,7 @@ PROCS = { "./encoderd": 13.0, "./camerad": 10.0, "selfdrive.controls.plannerd": 8.0, - "./ui": 18.0, + "selfdrive.ui.ui": 40.0, "system.sensord.sensord": 13.0, "selfdrive.controls.radard": 2.0, "selfdrive.modeld.modeld": 22.0, @@ -206,7 +206,8 @@ class TestOnroad: result += "-------------- UI Draw Timing ------------------\n" result += "------------------------------------------------\n" - ts = self.ts['uiDebug']['drawTimeMillis'] + # skip first few frames -- connecting to vipc + ts = self.ts['uiDebug']['drawTimeMillis'][15:] result += f"min {min(ts):.2f}ms\n" result += f"max {max(ts):.2f}ms\n" result += f"std {np.std(ts):.2f}ms\n" @@ -215,7 +216,7 @@ class TestOnroad: print(result) assert max(ts) < 250. - assert np.mean(ts) < 10. + assert np.mean(ts) < 20. # TODO: ~6-11ms, increase consistency #self.assertLess(np.std(ts), 5.) # some slow frames are expected since camerad/modeld can preempt ui @@ -285,7 +286,7 @@ class TestOnroad: # check for big leaks. note that memory usage is # expected to go up while the MSGQ buffers fill up - assert np.average(mems) <= 65, "Average memory usage above 65%" + assert np.average(mems) <= 85, "Average memory usage above 85%" assert np.max(np.diff(mems)) <= 4, "Max memory increase too high" assert np.average(np.diff(mems)) <= 1, "Average memory increase too high" diff --git a/selfdrive/ui/.gitignore b/selfdrive/ui/.gitignore index 7e9eaf932f..945928f617 100644 --- a/selfdrive/ui/.gitignore +++ b/selfdrive/ui/.gitignore @@ -1,14 +1 @@ -moc_* -*.moc - -translations/main_test_en.* - -ui -mui -watch3 installer/installers/* -qt/setup/setup -qt/setup/reset -qt/setup/wifi -qt/setup/updater -translations/alerts_generated.h diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 2a88832ef2..8a5f5793a5 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -1,75 +1,37 @@ +import os +import re import json -Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations') +from pathlib import Path +Import('env', 'arch', 'common') -base_libs = [common, messaging, visionipc, transformations, - 'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] +# build the fonts +generator = File("#selfdrive/assets/fonts/process.py") +source_files = Glob("#selfdrive/assets/fonts/*.ttf") + Glob("#selfdrive/assets/fonts/*.otf") +output_files = [ + (f.abspath.split('.')[0] + ".fnt", f.abspath.split('.')[0] + ".png") + for f in source_files + if "NotoColor" not in f.name +] +env.Command( + target=output_files, + source=[generator, source_files], + action=f"python3 {generator}", +) -if arch == 'larch64': - base_libs.append('EGL') - -if arch == "Darwin": - del base_libs[base_libs.index('OpenCL')] - qt_env['FRAMEWORKS'] += ['OpenCL'] - -sp_widgets_src = [] -sp_qt_src = [] -sp_qt_util = [] -if not GetOption('stock_ui'): - SConscript(['sunnypilot/SConscript']) - 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"] + 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", - "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 - -widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) -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/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/offroad/firehose.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 - -# build translation files +# compile gettext .po -> .mo translations with open(File("translations/languages.json").abspath) as f: languages = json.loads(f.read()) -translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()] -translation_targets = [src.replace(".ts", ".qm") for src in translation_sources] -lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease' -lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES") -qt_env.NoClean(translation_sources) -qt_env.Precious(translation_sources) +po_sources = [f"#selfdrive/ui/translations/app_{l}.po" for l in languages.values()] +po_sources = [src for src in po_sources if os.path.exists(File(src).abspath)] +mo_targets = [src.replace(".po", ".mo") for src in po_sources] +mo_build = [] +for src, tgt in zip(po_sources, mo_targets): + mo_build.append(env.Command(tgt, src, "msgfmt -o $TARGET $SOURCE")) +mo_alias = env.Alias('mo', mo_build) +env.AlwaysBuild(mo_alias) -# create qrc file for compiled translations to include with assets -translations_assets_src = "#selfdrive/assets/translations_assets.qrc" -with open(File(translations_assets_src).abspath, 'w') as f: - f.write('\n\n') - f.write('\n'.join([f'../ui/translations/{l}.qm' for l in languages.values()])) - f.write('\n\n') - -# build assets -assets = "#selfdrive/assets/assets.cc" -assets_src = "#selfdrive/assets/assets.qrc" -qt_env.Command(assets, [assets_src, translations_assets_src], f"rcc $SOURCES -o $TARGET") -qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, translations_assets_src, "#selfdrive/assets/assets.o"]) + [lrelease]) -asset_obj = qt_env.Object("assets", assets) - -# build main UI -qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs) if GetOption('extras'): - qt_src.remove("main.cc") # replaced by test_runner - qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) - # build installers if arch != "Darwin": raylib_env = env.Clone() @@ -78,7 +40,7 @@ if GetOption('extras'): raylib_libs = common + ["raylib"] if arch == "larch64": - raylib_libs += ["GLESv2", "wayland-client", "wayland-egl", "EGL"] + raylib_libs += ["GLESv2", "EGL", "gbm", "drm"] else: raylib_libs += ["GL"] diff --git a/selfdrive/ui/__init__.py b/selfdrive/ui/__init__.py index e69de29bb2..b07e842f1a 100644 --- a/selfdrive/ui/__init__.py +++ b/selfdrive/ui/__init__.py @@ -0,0 +1 @@ +UI_BORDER_SIZE = 30 diff --git a/selfdrive/ui/installer/installer.cc b/selfdrive/ui/installer/installer.cc index a9b84b5c06..5cb0a38e0b 100644 --- a/selfdrive/ui/installer/installer.cc +++ b/selfdrive/ui/installer/installer.cc @@ -5,6 +5,7 @@ #include "common/swaglog.h" #include "common/util.h" +#include "system/hardware/hw.h" #include "third_party/raylib/include/raylib.h" int freshClone(); @@ -38,6 +39,27 @@ extern const uint8_t inter_ttf_end[] asm("_binary_selfdrive_ui_installer_inter_a Font font; +std::vector tici_prebuilt_branches = {"release3", "release-tizi", "release3-staging", "nightly", "nightly-dev"}; +std::string migrated_branch; + +void branchMigration() { + migrated_branch = BRANCH_STR; + cereal::InitData::DeviceType device_type = Hardware::get_device_type(); + if (device_type == cereal::InitData::DeviceType::TICI) { + if (std::find(tici_prebuilt_branches.begin(), tici_prebuilt_branches.end(), BRANCH_STR) != tici_prebuilt_branches.end()) { + migrated_branch = "release-tici"; + } else if (BRANCH_STR == "master") { + migrated_branch = "master-tici"; + } + } else if (device_type == cereal::InitData::DeviceType::TIZI) { + if (BRANCH_STR == "release3") { + migrated_branch = "release-tizi"; + } else if (BRANCH_STR == "release3-staging") { + migrated_branch = "release-tizi-staging"; + } + } +} + void run(const char* cmd) { int err = std::system(cmd); assert(err == 0); @@ -87,7 +109,7 @@ int doInstall() { int freshClone() { LOGD("Doing fresh clone"); std::string cmd = util::string_format("git clone --progress %s -b %s --depth=1 --recurse-submodules %s 2>&1", - GIT_URL.c_str(), BRANCH_STR.c_str(), TMP_INSTALL_PATH); + GIT_URL.c_str(), migrated_branch.c_str(), TMP_INSTALL_PATH); return executeGitCommand(cmd); } @@ -95,11 +117,11 @@ int cachedFetch(const std::string &cache) { LOGD("Fetching with cache: %s", cache.c_str()); run(util::string_format("cp -rp %s %s", cache.c_str(), TMP_INSTALL_PATH).c_str()); - run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, BRANCH_STR.c_str()).c_str()); + run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, migrated_branch.c_str()).c_str()); renderProgress(10); - return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, BRANCH_STR.c_str())); + return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, migrated_branch.c_str())); } int executeGitCommand(const std::string &cmd) { @@ -142,8 +164,8 @@ void cloneFinished(int exitCode) { // ensure correct branch is checked out int err = chdir(TMP_INSTALL_PATH); assert(err == 0); - run(("git checkout " + BRANCH_STR).c_str()); - run(("git reset --hard origin/" + BRANCH_STR).c_str()); + run(("git checkout " + migrated_branch).c_str()); + run(("git reset --hard origin/" + migrated_branch).c_str()); run("git submodule update --init"); // move into place @@ -193,6 +215,8 @@ int main(int argc, char *argv[]) { font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0); SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); + branchMigration(); + if (util::file_exists(CONTINUE_PATH)) { finishInstall(); } else { diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index f102481c3a..7f477d4241 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -8,7 +8,9 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.multilang import tr, trn +from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets import Widget HEADER_HEIGHT = 80 @@ -35,12 +37,17 @@ class HomeLayout(Widget): self.update_alert = UpdateAlert() self.offroad_alert = OffroadAlert() + self._layout_widgets = {HomeLayoutState.UPDATE: self.update_alert, HomeLayoutState.ALERTS: self.offroad_alert} + self.current_state = HomeLayoutState.HOME self.last_refresh = 0 self.settings_callback: callable | None = None self.update_available = False self.alert_count = 0 + self._version_text = "" + self._prev_update_available = False + self._prev_alerts_present = False self.header_rect = rl.Rectangle(0, 0, 0, 0) self.content_rect = rl.Rectangle(0, 0, 0, 0) @@ -56,14 +63,30 @@ class HomeLayout(Widget): self._exp_mode_button = ExperimentalModeButton() self._setup_callbacks() + def show_event(self): + self._exp_mode_button.show_event() + self.last_refresh = time.monotonic() + self._refresh() + def _setup_callbacks(self): self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) + self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None) def set_settings_callback(self, callback: Callable): self.settings_callback = callback def _set_state(self, state: HomeLayoutState): + # propagate show/hide events + if state != self.current_state: + if state == HomeLayoutState.HOME: + self._exp_mode_button.show_event() + + if state in self._layout_widgets: + self._layout_widgets[state].show_event() + if self.current_state in self._layout_widgets: + self._layout_widgets[self.current_state].hide_event() + self.current_state = state def _render(self, rect: rl.Rectangle): @@ -72,7 +95,6 @@ class HomeLayout(Widget): self._refresh() self.last_refresh = current_time - self._handle_input() self._render_header() # Render content based on current state @@ -83,7 +105,7 @@ class HomeLayout(Widget): elif self.current_state == HomeLayoutState.ALERTS: self._render_alerts_view() - def _update_layout_rects(self): + def _update_state(self): self.header_rect = rl.Rectangle( self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT ) @@ -110,59 +132,54 @@ class HomeLayout(Widget): self.alert_notif_rect.x = notif_x self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2 - def _handle_input(self): - if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): - return - - mouse_pos = rl.get_mouse_position() + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect): self._set_state(HomeLayoutState.UPDATE) - return - - if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect): + elif self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect): self._set_state(HomeLayoutState.ALERTS) - return - - # Content area input handling - if self.current_state == HomeLayoutState.UPDATE: - self.update_alert.handle_input(mouse_pos, True) - elif self.current_state == HomeLayoutState.ALERTS: - self.offroad_alert.handle_input(mouse_pos, True) def _render_header(self): font = gui_app.font(FontWeight.MEDIUM) + version_text_width = self.header_rect.width + # Update notification button if self.update_available: + version_text_width -= self.update_notif_rect.width + # Highlight if currently viewing updates - highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255) + highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255) rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color) - text = "UPDATE" - text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x - text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2 - text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 + text = tr("UPDATE") + text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE) + text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2 + text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2 rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) # Alert notification button if self.alert_count > 0: + version_text_width -= self.alert_notif_rect.width + # Highlight if currently viewing alerts highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255) rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color) - alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}" - text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x - text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2 - text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 + alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count) + text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE) + text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2 + text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2 rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) # Version text (right aligned) - version_text = self._get_version_text() - text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x - version_x = self.header_rect.x + self.header_rect.width - text_width - version_y = self.header_rect.y + (self.header_rect.height - 48) // 2 - rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR) + if self.update_available or self.alert_count > 0: + version_text_width -= SPACING * 1.5 + + version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y, + version_text_width, self.header_rect.height) + gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) def _render_home_content(self): self._render_left_column() @@ -193,20 +210,23 @@ class HomeLayout(Widget): self._setup_widget.render(setup_rect) def _refresh(self): - # TODO: implement _update_state with a timer - self.update_available = self.update_alert.refresh() - self.alert_count = self.offroad_alert.refresh() - self._update_state_priority(self.update_available, self.alert_count > 0) - - def _update_state_priority(self, update_available: bool, alerts_present: bool): - current_state = self.current_state + self._version_text = self._get_version_text() + update_available = self.update_alert.refresh() + alert_count = self.offroad_alert.refresh() + alerts_present = alert_count > 0 + # Show panels on transition from no alert/update to any alerts/update if not update_available and not alerts_present: - self.current_state = HomeLayoutState.HOME - elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)): - self.current_state = HomeLayoutState.UPDATE - elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)): - self.current_state = HomeLayoutState.ALERTS + self._set_state(HomeLayoutState.HOME) + elif update_available and ((not self._prev_update_available) or (not alerts_present and self.current_state == HomeLayoutState.ALERTS)): + self._set_state(HomeLayoutState.UPDATE) + elif alerts_present and ((not self._prev_alerts_present) or (not update_available and self.current_state == HomeLayoutState.UPDATE)): + self._set_state(HomeLayoutState.ALERTS) + + self.update_available = update_available + self.alert_count = alert_count + self._prev_update_available = update_available + self._prev_alerts_present = alerts_present def _get_version_text(self) -> str: brand = "openpilot" diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index 777d2f4c3f..702854f98a 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -1,12 +1,14 @@ import pyray as rl from enum import IntEnum import cereal.messaging as messaging +from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH from openpilot.selfdrive.ui.layouts.home import HomeLayout from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.widgets import Widget +from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow class MainState(IntEnum): @@ -34,16 +36,23 @@ class MainLayout(Widget): # Set callbacks self._setup_callbacks() + # Start onboarding if terms or training not completed + self._onboarding_window = OnboardingWindow() + if not self._onboarding_window.completed: + gui_app.set_modal_overlay(self._onboarding_window) + def _render(self, _): self._handle_onroad_transition() self._render_main_content() def _setup_callbacks(self): self._sidebar.set_callbacks(on_settings=self._on_settings_clicked, - on_flag=self._on_bookmark_clicked) + on_flag=self._on_bookmark_clicked, + open_settings=lambda: self.open_settings(PanelType.TOGGLES)) self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE)) + self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES)) self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state) - self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked) + self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked) device.add_interactive_timeout_callback(self._set_mode_for_state) def _update_layout_rects(self): diff --git a/selfdrive/ui/layouts/onboarding.py b/selfdrive/ui/layouts/onboarding.py new file mode 100644 index 0000000000..df259a8fb5 --- /dev/null +++ b/selfdrive/ui/layouts/onboarding.py @@ -0,0 +1,214 @@ +import os +import re +import threading +from enum import IntEnum + +import pyray as rl +from openpilot.common.basedir import BASEDIR +from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import Button, ButtonStyle +from openpilot.system.ui.widgets.label import Label +from openpilot.selfdrive.ui.ui_state import ui_state + +DEBUG = False + +STEP_RECTS = [rl.Rectangle(104, 800, 633, 175), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2156, 1080), + rl.Rectangle(1526, 473, 427, 472), rl.Rectangle(1643, 441, 217, 223), rl.Rectangle(1835, 0, 2155, 1080), + rl.Rectangle(1786, 591, 267, 236), rl.Rectangle(1353, 0, 804, 1080), rl.Rectangle(1458, 485, 633, 211), + rl.Rectangle(95, 794, 1158, 187), rl.Rectangle(1560, 170, 392, 397), rl.Rectangle(1835, 0, 2159, 1080), + rl.Rectangle(1351, 0, 807, 1080), rl.Rectangle(1835, 0, 2158, 1080), rl.Rectangle(1531, 82, 441, 920), + rl.Rectangle(1336, 438, 490, 393), rl.Rectangle(1835, 0, 2159, 1080), rl.Rectangle(1835, 0, 2159, 1080), + rl.Rectangle(87, 795, 1187, 186)] + +DM_RECORD_STEP = 9 +DM_RECORD_YES_RECT = rl.Rectangle(695, 794, 558, 187) + +RESTART_TRAINING_RECT = rl.Rectangle(87, 795, 472, 186) + + +class OnboardingState(IntEnum): + TERMS = 0 + ONBOARDING = 1 + DECLINE = 2 + + +class TrainingGuide(Widget): + def __init__(self, completed_callback=None): + super().__init__() + self._completed_callback = completed_callback + + self._step = 0 + self._load_image_paths() + + # Load first image now so we show something immediately + self._textures = [gui_app.texture(self._image_paths[0])] + self._image_objs = [] + + threading.Thread(target=self._preload_thread, daemon=True).start() + + def _load_image_paths(self): + paths = [fn for fn in os.listdir(os.path.join(BASEDIR, "selfdrive/assets/training")) if re.match(r'^step\d*\.png$', fn)] + paths = sorted(paths, key=lambda x: int(re.search(r'\d+', x).group())) + self._image_paths = [os.path.join(BASEDIR, "selfdrive/assets/training", fn) for fn in paths] + + def _preload_thread(self): + # PNG loading is slow in raylib, so we preload in a thread and upload to GPU in main thread + # We've already loaded the first image on init + for path in self._image_paths[1:]: + self._image_objs.append(gui_app._load_image_from_path(path)) + + def _handle_mouse_release(self, mouse_pos): + if rl.check_collision_point_rec(mouse_pos, STEP_RECTS[self._step]): + # Record DM camera? + if self._step == DM_RECORD_STEP: + yes = rl.check_collision_point_rec(mouse_pos, DM_RECORD_YES_RECT) + print(f"putting RecordFront to {yes}") + ui_state.params.put_bool("RecordFront", yes) + + # Restart training? + elif self._step == len(self._image_paths) - 1: + if rl.check_collision_point_rec(mouse_pos, RESTART_TRAINING_RECT): + self._step = -1 + + self._step += 1 + + # Finished? + if self._step >= len(self._image_paths): + self._step = 0 + if self._completed_callback: + self._completed_callback() + + def _update_state(self): + if len(self._image_objs): + self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0))) + + def _render(self, _): + # Safeguard against fast tapping + step = min(self._step, len(self._textures) - 1) + rl.draw_texture(self._textures[step], 0, 0, rl.WHITE) + + # progress bar + if 0 < step < len(STEP_RECTS) - 1: + h = 20 + w = int((step / (len(STEP_RECTS) - 1)) * self._rect.width) + rl.draw_rectangle(int(self._rect.x), int(self._rect.y + self._rect.height - h), + w, h, rl.Color(70, 91, 234, 255)) + + if DEBUG: + rl.draw_rectangle_lines_ex(STEP_RECTS[step], 3, rl.RED) + + return -1 + + +class TermsPage(Widget): + def __init__(self, on_accept=None, on_decline=None): + super().__init__() + self._on_accept = on_accept + self._on_decline = on_decline + + self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."), + font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + + self._decline_btn = Button(tr("Decline"), click_callback=on_decline) + self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept) + + def _render(self, _): + welcome_x = self._rect.x + 165 + welcome_y = self._rect.y + 165 + welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90) + self._title.render(welcome_rect) + + desc_x = welcome_x + # TODO: Label doesn't top align when wrapping + desc_y = welcome_y - 100 + desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250) + self._desc.render(desc_rect) + + btn_y = self._rect.y + self._rect.height - 160 - 45 + btn_width = (self._rect.width - 45 * 3) / 2 + self._decline_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160)) + self._accept_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160)) + + if DEBUG: + rl.draw_rectangle_lines_ex(welcome_rect, 3, rl.RED) + rl.draw_rectangle_lines_ex(desc_rect, 3, rl.RED) + + return -1 + + +class DeclinePage(Widget): + def __init__(self, back_callback=None): + super().__init__() + self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."), + font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + self._back_btn = Button(tr("Back"), click_callback=back_callback) + self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER, + click_callback=self._on_uninstall_clicked) + + def _on_uninstall_clicked(self): + ui_state.params.put_bool("DoUninstall", True) + gui_app.request_close() + + def _render(self, _): + btn_y = self._rect.y + self._rect.height - 160 - 45 + btn_width = (self._rect.width - 45 * 3) / 2 + self._back_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160)) + self._uninstall_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160)) + + # text rect in middle of top and button + text_height = btn_y - (200 + 45) + text_rect = rl.Rectangle(self._rect.x + 165, self._rect.y + (btn_y - text_height) / 2 + 10, self._rect.width - (165 * 2), text_height) + if DEBUG: + rl.draw_rectangle_lines_ex(text_rect, 3, rl.RED) + self._text.render(text_rect) + + +class OnboardingWindow(Widget): + def __init__(self): + super().__init__() + self._current_terms_version = ui_state.params.get("TermsVersion") + self._current_training_version = ui_state.params.get("TrainingVersion") + self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == self._current_terms_version + self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == self._current_training_version + + self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING + + # Windows + self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined) + self._training_guide: TrainingGuide | None = None + self._decline_page = DeclinePage(back_callback=self._on_decline_back) + + @property + def completed(self) -> bool: + return self._accepted_terms and self._training_done + + def _on_terms_declined(self): + self._state = OnboardingState.DECLINE + + def _on_decline_back(self): + self._state = OnboardingState.TERMS + + def _on_terms_accepted(self): + ui_state.params.put("HasAcceptedTerms", self._current_terms_version) + self._state = OnboardingState.ONBOARDING + if self._training_done: + gui_app.set_modal_overlay(None) + + def _on_completed_training(self): + ui_state.params.put("CompletedTrainingVersion", self._current_training_version) + gui_app.set_modal_overlay(None) + + def _render(self, _): + if self._training_guide is None: + self._training_guide = TrainingGuide(completed_callback=self._on_completed_training) + + if self._state == OnboardingState.TERMS: + self._terms.render(self._rect) + if self._state == OnboardingState.ONBOARDING: + self._training_guide.render(self._rect) + elif self._state == OnboardingState.DECLINE: + self._decline_page.render(self._rect) + return -1 diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index cfef0f84d1..9ea1019f54 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -1,20 +1,30 @@ from openpilot.common.params import Params from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import toggle_item from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.widgets import DialogResult # Description constants DESCRIPTIONS = { - 'enable_adb': ( + 'enable_adb': tr_noop( "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." ), - 'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)", - 'ssh_key': ( + 'ssh_key': tr_noop( "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." ), + 'alpha_longitudinal': tr_noop( + "WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).

" + + "On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " + + "Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. " + + "Changing this setting will restart openpilot if the car is powered on." + ), } @@ -22,40 +32,154 @@ class DeveloperLayout(Widget): def __init__(self): super().__init__() self._params = Params() - items = [ - toggle_item( - "Enable ADB", - description=DESCRIPTIONS["enable_adb"], - initial_state=self._params.get_bool("AdbEnabled"), - callback=self._on_enable_adb, - ), - ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]), - toggle_item( - "Joystick Debug Mode", - description=DESCRIPTIONS["joystick_debug_mode"], - initial_state=self._params.get_bool("JoystickDebugMode"), - callback=self._on_joystick_debug_mode, - ), - toggle_item( - "Longitudinal Maneuver Mode", - description="", - initial_state=self._params.get_bool("LongitudinalManeuverMode"), - callback=self._on_long_maneuver_mode, - ), - toggle_item( - "openpilot Longitudinal Control (Alpha)", - description="", - initial_state=self._params.get_bool("AlphaLongitudinalEnabled"), - callback=self._on_alpha_long_enabled, - ), - ] + self._is_release = self._params.get_bool("IsReleaseBranch") - self._scroller = Scroller(items, line_separator=True, spacing=0) + # Build items and keep references for callbacks/state updates + self._adb_toggle = toggle_item( + lambda: tr("Enable ADB"), + description=lambda: tr(DESCRIPTIONS["enable_adb"]), + initial_state=self._params.get_bool("AdbEnabled"), + callback=self._on_enable_adb, + enabled=ui_state.is_offroad, + ) + + # SSH enable toggle + SSH key management + self._ssh_toggle = toggle_item( + lambda: tr("Enable SSH"), + description="", + initial_state=self._params.get_bool("SshEnabled"), + callback=self._on_enable_ssh, + ) + self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: tr(DESCRIPTIONS["ssh_key"])) + + self._joystick_toggle = toggle_item( + lambda: tr("Joystick Debug Mode"), + description="", + initial_state=self._params.get_bool("JoystickDebugMode"), + callback=self._on_joystick_debug_mode, + enabled=ui_state.is_offroad, + ) + + self._long_maneuver_toggle = toggle_item( + lambda: tr("Longitudinal Maneuver Mode"), + description="", + initial_state=self._params.get_bool("LongitudinalManeuverMode"), + callback=self._on_long_maneuver_mode, + ) + + self._alpha_long_toggle = toggle_item( + lambda: tr("openpilot Longitudinal Control (Alpha)"), + description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]), + initial_state=self._params.get_bool("AlphaLongitudinalEnabled"), + callback=self._on_alpha_long_enabled, + enabled=lambda: not ui_state.engaged, + ) + + self._ui_debug_toggle = toggle_item( + lambda: tr("UI Debug Mode"), + description="", + initial_state=self._params.get_bool("ShowDebugInfo"), + callback=self._on_enable_ui_debug, + ) + self._on_enable_ui_debug(self._params.get_bool("ShowDebugInfo")) + + self._scroller = Scroller([ + self._adb_toggle, + self._ssh_toggle, + self._ssh_keys, + self._joystick_toggle, + self._long_maneuver_toggle, + self._alpha_long_toggle, + self._ui_debug_toggle, + ], line_separator=True, spacing=0) + + # Toggles should be not available to change in onroad state + ui_state.add_offroad_transition_callback(self._update_toggles) def _render(self, rect): self._scroller.render(rect) - def _on_enable_adb(self): pass - def _on_joystick_debug_mode(self): pass - def _on_long_maneuver_mode(self): pass - def _on_alpha_long_enabled(self): pass + def show_event(self): + self._scroller.show_event() + self._update_toggles() + + def _update_toggles(self): + ui_state.update_params() + + # Hide non-release toggles on release builds + # TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault + for item in (self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle): + item.set_visible(not self._is_release) + + # CP gating + if ui_state.CP is not None: + alpha_avail = ui_state.CP.alphaLongitudinalAvailable + if not alpha_avail or self._is_release: + self._alpha_long_toggle.set_visible(False) + self._params.remove("AlphaLongitudinalEnabled") + else: + self._alpha_long_toggle.set_visible(True) + + long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad() + self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled) + if not long_man_enabled: + self._long_maneuver_toggle.action_item.set_state(False) + self._params.put_bool("LongitudinalManeuverMode", False) + else: + self._long_maneuver_toggle.action_item.set_enabled(False) + self._alpha_long_toggle.set_visible(False) + + # TODO: make a param control list item so we don't need to manage internal state as much here + # refresh toggles from params to mirror external changes + for key, item in ( + ("AdbEnabled", self._adb_toggle), + ("SshEnabled", self._ssh_toggle), + ("JoystickDebugMode", self._joystick_toggle), + ("LongitudinalManeuverMode", self._long_maneuver_toggle), + ("AlphaLongitudinalEnabled", self._alpha_long_toggle), + ("ShowDebugInfo", self._ui_debug_toggle), + ): + item.action_item.set_state(self._params.get_bool(key)) + + def _on_enable_ui_debug(self, state: bool): + self._params.put_bool("ShowDebugInfo", state) + gui_app.set_show_touches(state) + gui_app.set_show_fps(state) + + def _on_enable_adb(self, state: bool): + self._params.put_bool("AdbEnabled", state) + + def _on_enable_ssh(self, state: bool): + self._params.put_bool("SshEnabled", state) + + def _on_joystick_debug_mode(self, state: bool): + self._params.put_bool("JoystickDebugMode", state) + self._params.put_bool("LongitudinalManeuverMode", False) + self._long_maneuver_toggle.action_item.set_state(False) + + def _on_long_maneuver_mode(self, state: bool): + self._params.put_bool("LongitudinalManeuverMode", state) + self._params.put_bool("JoystickDebugMode", False) + self._joystick_toggle.action_item.set_state(False) + + def _on_alpha_long_enabled(self, state: bool): + if state: + def confirm_callback(result: int): + if result == DialogResult.CONFIRM: + self._params.put_bool("AlphaLongitudinalEnabled", True) + self._params.put_bool("OnroadCycleRequested", True) + self._update_toggles() + else: + self._alpha_long_toggle.action_item.set_state(False) + + # show confirmation dialog + content = (f"

{self._alpha_long_toggle.title}


" + + f"

{self._alpha_long_toggle.description}

") + + dlg = ConfirmDialog(content, tr("Enable"), rich=True) + gui_app.set_modal_overlay(dlg, callback=confirm_callback) + + else: + self._params.put_bool("AlphaLongitudinalEnabled", False) + self._params.put_bool("OnroadCycleRequested", True) + self._update_toggles() diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index df8bba030c..f5f37fbd3c 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -1,29 +1,30 @@ import os -import json +import math +from cereal import messaging, log from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.system.hardware import TICI -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop from openpilot.system.ui.widgets import Widget, DialogResult -from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog -from openpilot.system.ui.widgets.html_render import HtmlRenderer +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog +from openpilot.system.ui.widgets.html_render import HtmlModal from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.system.ui.widgets.scroller import Scroller # Description constants DESCRIPTIONS = { - 'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.", - 'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)", - 'reset_calibration': ( - "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." - ), - 'review_guide': "Review the rules, features, and limitations of openpilot", + 'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."), + 'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"), + 'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."), + 'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"), } @@ -35,49 +36,61 @@ class DeviceLayout(Widget): self._select_language_dialog: MultiOptionDialog | None = None self._driver_camera: DriverCameraDialog | None = None self._pair_device_dialog: PairingDialog | None = None - self._fcc_dialog: HtmlRenderer | None = None + self._fcc_dialog: HtmlModal | None = None + self._training_guide: TrainingGuide | None = None items = self._initialize_items() self._scroller = Scroller(items, line_separator=True, spacing=0) + ui_state.add_offroad_transition_callback(self._offroad_transition) + def _initialize_items(self): - dongle_id = self._params.get("DongleId") or "N/A" - serial = self._params.get("HardwareSerial") or "N/A" + self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device) + self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired()) + + self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']), + callback=self._reset_calibration_prompt) + self._reset_calib_btn.set_description_opened_callback(self._update_calib_description) + + self._power_off_btn = dual_button_item(lambda: tr("Reboot"), lambda: tr("Power Off"), + left_callback=self._reboot_prompt, right_callback=self._power_off_prompt) items = [ - text_item("Dongle ID", dongle_id), - text_item("Serial", serial), - button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device), - button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad), - button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt), - regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory), - button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide), - button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad), - dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt), + text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))), + text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))), + self._pair_device_btn, + button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']), + callback=self._show_driver_camera, enabled=ui_state.is_offroad), + self._reset_calib_btn, + button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']), + self._on_review_training_guide, enabled=ui_state.is_offroad), + regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad), + button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog), + self._power_off_btn, ] regulatory_btn.set_visible(TICI) return items + def _offroad_transition(self): + self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad()) + + def show_event(self): + self._scroller.show_event() + def _render(self, rect): self._scroller.render(rect) - def _show_language_selection(self): - try: - languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json") - with open(languages_file, encoding='utf-8') as f: - languages = json.load(f) + def _show_language_dialog(self): + def handle_language_selection(result: int): + if result == 1 and self._select_language_dialog: + selected_language = multilang.languages[self._select_language_dialog.selection] + multilang.change_language(selected_language) + self._update_calib_description() + self._select_language_dialog = None - self._select_language_dialog = MultiOptionDialog("Select a language", languages) - gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection) - except FileNotFoundError: - pass - - def _handle_language_selection(self, result: int): - if result == 1 and self._select_language_dialog: - selected_language = self._select_language_dialog.selection - self._params.put("LanguageSetting", selected_language) - - self._select_language_dialog = None + self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language], + option_font_weight=FontWeight.UNIFONT) + gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection) def _show_driver_camera(self): if not self._driver_camera: @@ -87,34 +100,80 @@ class DeviceLayout(Widget): def _reset_calibration_prompt(self): if ui_state.engaged: - gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration")) + gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration"))) return - gui_app.set_modal_overlay( - lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"), - callback=self._reset_calibration, - ) + def reset_calibration(result: int): + # Check engaged again in case it changed while the dialog was open + if ui_state.engaged or result != DialogResult.CONFIRM: + return - def _reset_calibration(self, result: int): - if ui_state.engaged or result != DialogResult.CONFIRM: - return + self._params.remove("CalibrationParams") + self._params.remove("LiveTorqueParameters") + self._params.remove("LiveParameters") + self._params.remove("LiveParametersV2") + self._params.remove("LiveDelay") + self._params.put_bool("OnroadCycleRequested", True) + self._update_calib_description() - self._params.remove("CalibrationParams") - self._params.remove("LiveTorqueParameters") - self._params.remove("LiveParameters") - self._params.remove("LiveParametersV2") - self._params.remove("LiveDelay") - self._params.put_bool("OnroadCycleRequested", True) + dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset")) + gui_app.set_modal_overlay(dialog, callback=reset_calibration) + + def _update_calib_description(self): + desc = tr(DESCRIPTIONS['reset_calibration']) + + calib_bytes = self._params.get("CalibrationParams") + if calib_bytes: + try: + calib = messaging.log_from_bytes(calib_bytes, log.Event).liveCalibration + + if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated: + pitch = math.degrees(calib.rpyCalib[1]) + yaw = math.degrees(calib.rpyCalib[2]) + desc += tr(" Your device is pointed {:.1f}° {} and {:.1f}° {}.").format(abs(pitch), tr("down") if pitch > 0 else tr("up"), + abs(yaw), tr("left") if yaw > 0 else tr("right")) + except Exception: + cloudlog.exception("invalid CalibrationParams") + + lag_perc = 0 + lag_bytes = self._params.get("LiveDelay") + if lag_bytes: + try: + lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).liveDelay.calPerc + except Exception: + cloudlog.exception("invalid LiveDelay") + if lag_perc < 100: + desc += tr("

Steering lag calibration is {}% complete.").format(lag_perc) + else: + desc += tr("

Steering lag calibration is complete.") + + torque_bytes = self._params.get("LiveTorqueParameters") + if torque_bytes: + try: + torque = messaging.log_from_bytes(torque_bytes, log.Event).liveTorqueParameters + # don't add for non-torque cars + if torque.useParams: + torque_perc = torque.calPerc + if torque_perc < 100: + desc += tr(" Steering torque response calibration is {}% complete.").format(torque_perc) + else: + desc += tr(" Steering torque response calibration is complete.") + except Exception: + cloudlog.exception("invalid LiveTorqueParameters") + + desc += "

" + desc += tr("openpilot is continuously calibrating, resetting is rarely required. " + + "Resetting calibration will restart openpilot if the car is powered on.") + + self._reset_calib_btn.set_description(desc) def _reboot_prompt(self): if ui_state.engaged: - gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot")) + gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot"))) return - gui_app.set_modal_overlay( - lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"), - callback=self._perform_reboot, - ) + dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot")) + gui_app.set_modal_overlay(dialog, callback=self._perform_reboot) def _perform_reboot(self, result: int): if not ui_state.engaged and result == DialogResult.CONFIRM: @@ -122,13 +181,11 @@ class DeviceLayout(Widget): def _power_off_prompt(self): if ui_state.engaged: - gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off")) + gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off"))) return - gui_app.set_modal_overlay( - lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"), - callback=self._perform_power_off, - ) + dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off")) + gui_app.set_modal_overlay(dialog, callback=self._perform_power_off) def _perform_power_off(self, result: int): if not ui_state.engaged and result == DialogResult.CONFIRM: @@ -141,10 +198,13 @@ class DeviceLayout(Widget): def _on_regulatory(self): if not self._fcc_dialog: - self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html")) + self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html")) + gui_app.set_modal_overlay(self._fcc_dialog) - gui_app.set_modal_overlay(self._fcc_dialog, - callback=lambda result: setattr(self, '_fcc_dialog', None), - ) + def _on_review_training_guide(self): + if not self._training_guide: + def completed_callback(): + gui_app.set_modal_overlay(None) - def _on_review_training_guide(self): pass + self._training_guide = TrainingGuide(completed_callback=completed_callback) + gui_app.set_modal_overlay(self._training_guide) diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index b3db1fa5f0..bbd4aef532 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -7,21 +7,23 @@ from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr, trn, tr_noop +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.lib.api_helpers import get_token -TITLE = "Firehose Mode" -DESCRIPTION = ( +TITLE = tr_noop("Firehose Mode") +DESCRIPTION = tr_noop( "openpilot learns to drive by watching humans, like you, drive.\n\n" + "Firehose Mode allows you to maximize your training data uploads to improve " + "openpilot's driving models. More data means bigger models, which means better Experimental Mode." ) -INSTRUCTIONS = ( +INSTRUCTIONS = tr_noop( "For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n" - + "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n" + + "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n" + "Frequently Asked Questions\n\n" + "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n" + "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n" @@ -43,12 +45,16 @@ class FirehoseLayout(Widget): self.params = Params() self.segment_count = self._get_segment_count() self.scroll_panel = GuiScrollPanel() + self._content_height = 0 self.running = True self.update_thread = threading.Thread(target=self._update_loop, daemon=True) self.update_thread.start() self.last_update_time = 0 + def show_event(self): + self.scroll_panel.set_offset(0) + def _get_segment_count(self) -> int: stats = self.params.get(self.PARAM_KEY) if not stats: @@ -66,97 +72,72 @@ class FirehoseLayout(Widget): def _render(self, rect: rl.Rectangle): # Calculate content dimensions - content_width = rect.width - 80 - content_height = self._calculate_content_height(int(content_width)) - content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) + content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height) # Handle scrolling and render with clipping - scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect) + scroll_offset = self.scroll_panel.update(rect, content_rect) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) - self._render_content(rect, scroll_offset) + self._content_height = self._render_content(rect, scroll_offset) rl.end_scissor_mode() - def _calculate_content_height(self, content_width: int) -> int: - height = 80 # Top margin - - # Title - height += 100 + 40 - - # Description - desc_font = gui_app.font(FontWeight.NORMAL) - desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width) - height += len(desc_lines) * 45 + 40 - - # Status section - height += 32 # Separator - status_text, _ = self._get_status() - status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width) - height += len(status_lines) * 60 + 20 - - # Contribution count (if available) - if self.segment_count > 0: - contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." - contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width) - height += len(contrib_lines) * 52 + 20 - - # Instructions section - height += 32 # Separator - inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width) - height += len(inst_lines) * 40 + 40 # Bottom margin - - return height - - def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2): + def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int: x = int(rect.x + 40) - y = int(rect.y + 40 + scroll_offset.y) + y = int(rect.y + 40 + scroll_offset) w = int(rect.width - 80) - # Title + # Title (centered) + title_text = tr(TITLE) # live translate title_font = gui_app.font(FontWeight.MEDIUM) - rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE) - y += 140 + text_width = measure_text_cached(title_font, title_text, 100).x + title_x = rect.x + (rect.width - text_width) / 2 + rl.draw_text_ex(title_font, title_text, rl.Vector2(title_x, y), 100, 0, rl.WHITE) + y += 200 # Description - y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE) - y += 40 + y = self._draw_wrapped_text(x, y, w, tr(DESCRIPTION), gui_app.font(FontWeight.NORMAL), 45, rl.WHITE) + y += 40 + 20 # Separator rl.draw_rectangle(x, y, w, 2, self.GRAY) - y += 30 + y += 30 + 20 # Status status_text, status_color = self._get_status() y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color) - y += 20 + y += 20 + 20 # Contribution count (if available) if self.segment_count > 0: - contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." + contrib_text = trn("{} segment of your driving is in the training dataset so far.", + "{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) - y += 20 + y += 20 + 20 # Separator rl.draw_rectangle(x, y, w, 2, self.GRAY) - y += 30 + y += 30 + 20 # Instructions - self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) + y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) - def _draw_wrapped_text(self, x, y, width, text, font, size, color): - wrapped = wrap_text(font, text, size, width) + # bottom margin + remove effect of scroll offset + return int(round(y - self.scroll_panel.offset + 40)) + + def _draw_wrapped_text(self, x, y, width, text, font, font_size, color): + wrapped = wrap_text(font, text, font_size, width) for line in wrapped: - rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color) - y += size - return y + rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color) + y += font_size * FONT_SCALE + return round(y) def _get_status(self) -> tuple[str, rl.Color]: network_type = ui_state.sm["deviceState"].networkType network_metered = ui_state.sm["deviceState"].networkMetered if not network_metered and network_type != 0: # Not metered and connected - return "ACTIVE", self.GREEN + return tr("ACTIVE"), self.GREEN else: - return "INACTIVE: connect to an unmetered network", self.RED + return tr("INACTIVE: connect to an unmetered network"), self.RED def _fetch_firehose_stats(self): try: diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index a731a9158c..72d3a4bafe 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -8,18 +8,16 @@ from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.network import WifiManagerUI - -# Settings close button -SETTINGS_CLOSE_TEXT = "×" -SETTINGS_CLOSE_TEXT_Y_OFFSET = 8 # The '×' character isn't quite vertically centered in the font so we need to offset it a bit to fully center it +from openpilot.system.ui.widgets.network import NetworkUI # Constants SIDEBAR_WIDTH = 500 CLOSE_BTN_SIZE = 200 +CLOSE_ICON_SIZE = 70 NAV_BTN_HEIGHT = 110 PANEL_MARGIN = 50 @@ -58,15 +56,16 @@ class SettingsLayout(Widget): wifi_manager.set_active(False) self._panels = { - PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), - PanelType.NETWORK: PanelInfo("Network", WifiManagerUI(wifi_manager)), - PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), - PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), - PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), - PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()), + PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()), + PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)), + PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()), + PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()), + PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()), + PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()), } self._font_medium = gui_app.font(FontWeight.MEDIUM) + self._close_icon = gui_app.texture("icons/close2.png", CLOSE_ICON_SIZE, CLOSE_ICON_SIZE) # Callbacks self._close_callback: Callable | None = None @@ -96,12 +95,21 @@ class SettingsLayout(Widget): close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color) - close_text_size = measure_text_cached(self._font_medium, SETTINGS_CLOSE_TEXT, 140) - close_text_pos = rl.Vector2( - close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2, - close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2 - SETTINGS_CLOSE_TEXT_Y_OFFSET, + icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255) + icon_dest = rl.Rectangle( + close_btn_rect.x + (close_btn_rect.width - self._close_icon.width) / 2, + close_btn_rect.y + (close_btn_rect.height - self._close_icon.height) / 2, + self._close_icon.width, + self._close_icon.height, + ) + rl.draw_texture_pro( + self._close_icon, + rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height), + icon_dest, + rl.Vector2(0, 0), + 0, + icon_color, ) - rl.draw_text_ex(self._font_medium, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED) # Store close button rect for click detection self._close_btn_rect = close_btn_rect @@ -115,11 +123,12 @@ class SettingsLayout(Widget): is_selected = panel_type == self._current_panel text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL # Draw button text (right-aligned) - text_size = measure_text_cached(self._font_medium, panel_info.name, 65) + panel_name = tr(panel_info.name) + text_size = measure_text_cached(self._font_medium, panel_name, 65) text_pos = rl.Vector2( button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2 ) - rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color) + rl.draw_text_ex(self._font_medium, panel_name, text_pos, 65, 0, text_color) # Store button rect for click detection panel_info.button_rect = button_rect diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index 4361725a1b..8166a8a9e4 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -1,42 +1,194 @@ -from openpilot.common.params import Params +import os +import time +import datetime +from openpilot.common.time_helpers import system_time_valid +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr, trn from openpilot.system.ui.widgets import Widget, DialogResult -from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog -from openpilot.system.ui.widgets.list_view import button_item, text_item +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.system.ui.widgets.scroller import Scroller +# TODO: remove this. updater fails to respond on startup if time is not correct +UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond + + +def time_ago(date: datetime.datetime | None) -> str: + if not date: + return tr("never") + + if not system_time_valid(): + return date.strftime("%a %b %d %Y") + + now = datetime.datetime.now(datetime.UTC) + if date.tzinfo is None: + date = date.replace(tzinfo=datetime.UTC) + + diff_seconds = int((now - date).total_seconds()) + if diff_seconds < 60: + return tr("now") + if diff_seconds < 3600: + m = diff_seconds // 60 + return trn("{} minute ago", "{} minutes ago", m).format(m) + if diff_seconds < 86400: + h = diff_seconds // 3600 + return trn("{} hour ago", "{} hours ago", h).format(h) + if diff_seconds < 604800: + d = diff_seconds // 86400 + return trn("{} day ago", "{} days ago", d).format(d) + return date.strftime("%a %b %d %Y") + class SoftwareLayout(Widget): def __init__(self): super().__init__() - self._params = Params() - items = self._init_items() - self._scroller = Scroller(items, line_separator=True, spacing=0) + self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off.")) + self._version_item = text_item(lambda: tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "") + self._download_btn = button_item(lambda: tr("Download"), lambda: tr("CHECK"), callback=self._on_download_update) - def _init_items(self): - items = [ - text_item("Current Version", ""), - button_item("Download", "CHECK", callback=self._on_download_update), - button_item("Install Update", "INSTALL", callback=self._on_install_update), - button_item("Target Branch", "SELECT", callback=self._on_select_branch), - button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall), - ] - return items + # Install button is initially hidden + self._install_btn = button_item(lambda: tr("Install Update"), lambda: tr("INSTALL"), callback=self._on_install_update) + self._install_btn.set_visible(False) + + # Track waiting-for-updater transition to avoid brief re-enable while still idle + self._waiting_for_updater = False + self._waiting_start_ts: float = 0.0 + + # Branch switcher + self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch) + self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch")) + self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "") + self._branch_dialog: MultiOptionDialog | None = None + + self._scroller = Scroller([ + self._onroad_label, + self._version_item, + self._download_btn, + self._install_btn, + self._branch_btn, + button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall), + ], line_separator=True, spacing=0) + + def show_event(self): + self._scroller.show_event() def _render(self, rect): self._scroller.render(rect) - def _on_download_update(self): pass - def _on_install_update(self): pass - def _on_select_branch(self): pass + def _update_state(self): + # Show/hide onroad warning + self._onroad_label.set_visible(ui_state.is_onroad()) + + # Update current version and release notes + current_desc = ui_state.params.get("UpdaterCurrentDescription") or "" + current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace") + self._version_item.action_item.set_text(current_desc) + self._version_item.set_description(current_release_notes) + + # Update download button visibility and state + self._download_btn.set_visible(ui_state.is_offroad()) + + updater_state = ui_state.params.get("UpdaterState") or "idle" + failed_count = ui_state.params.get("UpdateFailedCount") or 0 + fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable") + update_available = ui_state.params.get_bool("UpdateAvailable") + + if updater_state != "idle": + # Updater responded + self._waiting_for_updater = False + self._download_btn.action_item.set_enabled(False) + self._download_btn.action_item.set_value(updater_state) + else: + if failed_count > 0: + self._download_btn.action_item.set_value(tr("failed to check for update")) + self._download_btn.action_item.set_text(tr("CHECK")) + elif fetch_available: + self._download_btn.action_item.set_value(tr("update available")) + self._download_btn.action_item.set_text(tr("DOWNLOAD")) + else: + last_update = ui_state.params.get("LastUpdateTime") + if last_update: + formatted = time_ago(last_update) + self._download_btn.action_item.set_value(tr("up to date, last checked {}").format(formatted)) + else: + self._download_btn.action_item.set_value(tr("up to date, last checked never")) + self._download_btn.action_item.set_text(tr("CHECK")) + + # If we've been waiting too long without a state change, reset state + if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT): + self._waiting_for_updater = False + + # Only enable if we're not waiting for updater to flip out of idle + self._download_btn.action_item.set_enabled(not self._waiting_for_updater) + + # Update target branch button value + current_branch = ui_state.params.get("UpdaterTargetBranch") or "" + self._branch_btn.action_item.set_value(current_branch) + + # Update install button + self._install_btn.set_visible(ui_state.is_offroad() and update_available) + if update_available: + new_desc = ui_state.params.get("UpdaterNewDescription") or "" + new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace") + self._install_btn.action_item.set_text(tr("INSTALL")) + self._install_btn.action_item.set_value(new_desc) + self._install_btn.set_description(new_release_notes) + # Enable install button for testing (like Qt showEvent) + self._install_btn.action_item.set_enabled(True) + else: + self._install_btn.set_visible(False) + + def _on_download_update(self): + # Check if we should start checking or start downloading + self._download_btn.action_item.set_enabled(False) + if self._download_btn.action_item.text == tr("CHECK"): + # Start checking for updates + self._waiting_for_updater = True + self._waiting_start_ts = time.monotonic() + os.system("pkill -SIGUSR1 -f system.updated.updated") + else: + # Start downloading + self._waiting_for_updater = True + self._waiting_start_ts = time.monotonic() + os.system("pkill -SIGHUP -f system.updated.updated") def _on_uninstall(self): def handle_uninstall_confirmation(result): if result == DialogResult.CONFIRM: - self._params.put_bool("DoUninstall", True) + ui_state.params.put_bool("DoUninstall", True) - gui_app.set_modal_overlay( - lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"), - callback=handle_uninstall_confirmation, - ) + dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall")) + gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation) + + def _on_install_update(self): + # Trigger reboot to install update + self._install_btn.action_item.set_enabled(False) + ui_state.params.put_bool("DoReboot", True) + + def _on_select_branch(self): + # Get available branches and order + current_git_branch = ui_state.params.get("GitBranch") or "" + branches_str = ui_state.params.get("UpdaterAvailableBranches") or "" + branches = [b for b in branches_str.split(",") if b] + + for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]: + if b in branches: + branches.remove(b) + branches.insert(0, b) + + current_target = ui_state.params.get("UpdaterTargetBranch") or "" + self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target) + + def handle_selection(result): + # Confirmed selection + if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection: + selection = self._branch_dialog.selection + ui_state.params.put("UpdaterTargetBranch", selection) + self._branch_btn.action_item.set_value(selection) + os.system("pkill -SIGUSR1 -f system.updated.updated") + self._branch_dialog = None + + gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection) diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 58afcec5ef..3a1265e0fe 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -1,28 +1,36 @@ -from openpilot.common.params import Params +from cereal import log +from openpilot.common.params import Params, UnknownKeyName from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.widgets import DialogResult +from openpilot.selfdrive.ui.ui_state import ui_state + +PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants # Description constants DESCRIPTIONS = { - "OpenpilotEnabledToggle": ( + "OpenpilotEnabledToggle": tr_noop( "Use the openpilot system for adaptive cruise control and lane keep driver assistance. " + "Your attention is required at all times to use this feature." ), - "DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.", - "LongitudinalPersonality": ( + "DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."), + "LongitudinalPersonality": tr_noop( "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." ), - "IsLdwEnabled": ( + "IsLdwEnabled": tr_noop( "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)." ), - "AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.", - 'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", - "IsMetric": "Display speed in km/h instead of mph.", - "RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.", + "AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."), + 'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), + "IsMetric": tr_noop("Display speed in km/h instead of mph."), + "RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), } @@ -30,66 +38,207 @@ class TogglesLayout(Widget): def __init__(self): super().__init__() self._params = Params() - items = [ - toggle_item( - "Enable openpilot", - DESCRIPTIONS["OpenpilotEnabledToggle"], - self._params.get_bool("OpenpilotEnabledToggle"), - icon="chffr_wheel.png", - ), - toggle_item( - "Experimental Mode", - initial_state=self._params.get_bool("ExperimentalMode"), - icon="experimental_white.png", - ), - toggle_item( - "Disengage on Accelerator Pedal", - DESCRIPTIONS["DisengageOnAccelerator"], - self._params.get_bool("DisengageOnAccelerator"), - icon="disengage_on_accelerator.png", - ), - multiple_button_item( - "Driving Personality", - DESCRIPTIONS["LongitudinalPersonality"], - buttons=["Aggressive", "Standard", "Relaxed"], - button_width=255, - callback=self._set_longitudinal_personality, - selected_index=self._params.get("LongitudinalPersonality", return_default=True), - icon="speed_limit.png" - ), - toggle_item( - "Enable Lane Departure Warnings", - DESCRIPTIONS["IsLdwEnabled"], - self._params.get_bool("IsLdwEnabled"), - icon="warning.png", - ), - toggle_item( - "Always-On Driver Monitoring", - DESCRIPTIONS["AlwaysOnDM"], - self._params.get_bool("AlwaysOnDM"), - icon="monitoring.png", - ), - toggle_item( - "Record and Upload Driver Camera", - DESCRIPTIONS["RecordFront"], - self._params.get_bool("RecordFront"), - icon="monitoring.png", - ), - toggle_item( - "Record Microphone Audio", - DESCRIPTIONS["RecordAudio"], - self._params.get_bool("RecordAudio"), - icon="microphone.png", - ), - toggle_item( - "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png" - ), - ] + self._is_release = self._params.get_bool("IsReleaseBranch") - self._scroller = Scroller(items, line_separator=True, spacing=0) + # param, title, desc, icon, needs_restart + self._toggle_defs = { + "OpenpilotEnabledToggle": ( + lambda: tr("Enable openpilot"), + DESCRIPTIONS["OpenpilotEnabledToggle"], + "chffr_wheel.png", + True, + ), + "ExperimentalMode": ( + lambda: tr("Experimental Mode"), + "", + "experimental_white.png", + False, + ), + "DisengageOnAccelerator": ( + lambda: tr("Disengage on Accelerator Pedal"), + DESCRIPTIONS["DisengageOnAccelerator"], + "disengage_on_accelerator.png", + False, + ), + "IsLdwEnabled": ( + lambda: tr("Enable Lane Departure Warnings"), + DESCRIPTIONS["IsLdwEnabled"], + "warning.png", + False, + ), + "AlwaysOnDM": ( + lambda: tr("Always-On Driver Monitoring"), + DESCRIPTIONS["AlwaysOnDM"], + "monitoring.png", + False, + ), + "RecordFront": ( + lambda: tr("Record and Upload Driver Camera"), + DESCRIPTIONS["RecordFront"], + "monitoring.png", + True, + ), + "RecordAudio": ( + lambda: tr("Record and Upload Microphone Audio"), + DESCRIPTIONS["RecordAudio"], + "microphone.png", + True, + ), + "IsMetric": ( + lambda: tr("Use Metric System"), + DESCRIPTIONS["IsMetric"], + "metric.png", + False, + ), + } + + self._long_personality_setting = multiple_button_item( + lambda: tr("Driving Personality"), + lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]), + buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")], + button_width=255, + callback=self._set_longitudinal_personality, + selected_index=self._params.get("LongitudinalPersonality", return_default=True), + icon="speed_limit.png" + ) + + self._toggles = {} + self._locked_toggles = set() + for param, (title, desc, icon, needs_restart) in self._toggle_defs.items(): + toggle = toggle_item( + title, + desc, + self._params.get_bool(param), + callback=lambda state, p=param: self._toggle_callback(state, p), + icon=icon, + ) + + try: + locked = self._params.get_bool(param + "Lock") + except UnknownKeyName: + locked = False + toggle.action_item.set_enabled(not locked) + + # Make description callable for live translation + additional_desc = "" + if needs_restart and not locked: + additional_desc = tr("Changing this setting will restart openpilot if the car is powered on.") + toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else "")) + + # track for engaged state updates + if locked: + self._locked_toggles.add(param) + + self._toggles[param] = toggle + + # insert longitudinal personality after NDOG toggle + if param == "DisengageOnAccelerator": + self._toggles["LongitudinalPersonality"] = self._long_personality_setting + + self._update_experimental_mode_icon() + self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0) + + ui_state.add_engaged_transition_callback(self._update_toggles) + + def _update_state(self): + if ui_state.sm.updated["selfdriveState"]: + personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality] + if personality != ui_state.personality and ui_state.started: + self._long_personality_setting.action_item.set_selected_button(personality) + ui_state.personality = personality + + def show_event(self): + self._scroller.show_event() + self._update_toggles() + + def _update_toggles(self): + ui_state.update_params() + + e2e_description = 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:
" + + "

End-to-End Longitudinal Control


" + + "Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " + + "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " + + "mistakes should be expected.
" + + "

New Driving Visualization


" + + "The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " + + "The Experimental mode logo will also be shown in the top right corner." + ) + + if ui_state.CP is not None: + if ui_state.has_longitudinal_control: + self._toggles["ExperimentalMode"].action_item.set_enabled(True) + self._toggles["ExperimentalMode"].set_description(e2e_description) + self._long_personality_setting.action_item.set_enabled(True) + else: + # no long for now + self._toggles["ExperimentalMode"].action_item.set_enabled(False) + self._toggles["ExperimentalMode"].action_item.set_state(False) + self._long_personality_setting.action_item.set_enabled(False) + self._params.remove("ExperimentalMode") + + unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.") + + long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.") + if ui_state.CP.alphaLongitudinalAvailable: + if self._is_release: + long_desc = unavailable + " " + tr("An alpha version of openpilot 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.") + + self._toggles["ExperimentalMode"].set_description("" + long_desc + "

" + e2e_description) + else: + self._toggles["ExperimentalMode"].set_description(e2e_description) + + self._update_experimental_mode_icon() + + # TODO: make a param control list item so we don't need to manage internal state as much here + # refresh toggles from params to mirror external changes + for param in self._toggle_defs: + self._toggles[param].action_item.set_state(self._params.get_bool(param)) + + # these toggles need restart, block while engaged + for toggle_def in self._toggle_defs: + if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles: + self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged) def _render(self, rect): self._scroller.render(rect) + def _update_experimental_mode_icon(self): + icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png" + self._toggles["ExperimentalMode"].set_icon(icon) + + def _handle_experimental_mode_toggle(self, state: bool): + confirmed = self._params.get_bool("ExperimentalModeConfirmed") + if state and not confirmed: + def confirm_callback(result: int): + if result == DialogResult.CONFIRM: + self._params.put_bool("ExperimentalMode", True) + self._params.put_bool("ExperimentalModeConfirmed", True) + else: + self._toggles["ExperimentalMode"].action_item.set_state(False) + self._update_experimental_mode_icon() + + # show confirmation dialog + content = (f"

{self._toggles['ExperimentalMode'].title}


" + + f"

{self._toggles['ExperimentalMode'].description}

") + dlg = ConfirmDialog(content, tr("Enable"), rich=True) + gui_app.set_modal_overlay(dlg, callback=confirm_callback) + else: + self._update_experimental_mode_icon() + self._params.put_bool("ExperimentalMode", state) + + def _toggle_callback(self, state: bool, param: str): + if param == "ExperimentalMode": + self._handle_experimental_mode_toggle(state) + return + + self._params.put_bool(param, state) + if self._toggle_defs[param][3]: + self._params.put_bool("OnroadCycleRequested", True) + def _set_longitudinal_personality(self, button_index: int): self._params.put("LongitudinalPersonality", button_index) diff --git a/selfdrive/ui/layouts/sidebar.py b/selfdrive/ui/layouts/sidebar.py index cdbfa4c04c..d468442b15 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/selfdrive/ui/layouts/sidebar.py @@ -4,7 +4,8 @@ from dataclasses import dataclass from collections.abc import Callable from cereal import log from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -23,7 +24,6 @@ NetworkType = log.DeviceState.NetworkType # Color scheme class Colors: - SIDEBAR_BG = rl.Color(57, 57, 57, 255) WHITE = rl.WHITE WHITE_DIM = rl.Color(255, 255, 255, 85) GRAY = rl.Color(84, 84, 84, 255) @@ -40,13 +40,13 @@ class Colors: NETWORK_TYPES = { - NetworkType.none: "Offline", - NetworkType.wifi: "WiFi", - NetworkType.cell2G: "2G", - NetworkType.cell3G: "3G", - NetworkType.cell4G: "LTE", - NetworkType.cell5G: "5G", - NetworkType.ethernet: "Ethernet", + NetworkType.none: tr_noop("--"), + NetworkType.wifi: tr_noop("Wi-Fi"), + NetworkType.ethernet: tr_noop("ETH"), + NetworkType.cell2G: tr_noop("2G"), + NetworkType.cell3G: tr_noop("3G"), + NetworkType.cell4G: tr_noop("LTE"), + NetworkType.cell5G: tr_noop("5G"), } @@ -68,27 +68,33 @@ class Sidebar(Widget): self._net_type = NETWORK_TYPES.get(NetworkType.none) self._net_strength = 0 - self._temp_status = MetricData("TEMP", "GOOD", Colors.GOOD) - self._panda_status = MetricData("VEHICLE", "ONLINE", Colors.GOOD) - self._connect_status = MetricData("CONNECT", "OFFLINE", Colors.WARNING) + self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD) + self._panda_status = MetricData(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD) + self._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING) + self._recording_audio = False self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height) self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height) self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height) + self._mic_img = gui_app.texture("icons/microphone.png", 30, 30) + self._mic_indicator_rect = rl.Rectangle(0, 0, 0, 0) self._font_regular = gui_app.font(FontWeight.NORMAL) self._font_bold = gui_app.font(FontWeight.SEMI_BOLD) # Callbacks self._on_settings_click: Callable | None = None self._on_flag_click: Callable | None = None + self._open_settings_callback: Callable | None = None - def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None): + def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None, + open_settings: Callable | None = None): self._on_settings_click = on_settings self._on_flag_click = on_flag + self._open_settings_callback = open_settings def _render(self, rect: rl.Rectangle): # Background - rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG) + rl.draw_rectangle_rec(rect, rl.BLACK) self._draw_buttons(rect) self._draw_network_indicator(rect) @@ -101,13 +107,14 @@ class Sidebar(Widget): device_state = sm['deviceState'] + self._recording_audio = ui_state.recording_audio self._update_network_status(device_state) self._update_temperature_status(device_state) self._update_connection_status(device_state) self._update_panda_status() def _update_network_status(self, device_state): - self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, "Unknown") + self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown")) strength = device_state.networkStrength self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0 @@ -115,26 +122,26 @@ class Sidebar(Widget): thermal_status = device_state.thermalStatus if thermal_status == ThermalStatus.green: - self._temp_status.update("TEMP", "GOOD", Colors.GOOD) + self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD) elif thermal_status == ThermalStatus.yellow: - self._temp_status.update("TEMP", "OK", Colors.WARNING) + self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING) else: - self._temp_status.update("TEMP", "HIGH", Colors.DANGER) + self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER) def _update_connection_status(self, device_state): last_ping = device_state.lastAthenaPingTime if last_ping == 0: - self._connect_status.update("CONNECT", "OFFLINE", Colors.WARNING) + self._connect_status.update(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING) elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds - self._connect_status.update("CONNECT", "ONLINE", Colors.GOOD) + self._connect_status.update(tr_noop("CONNECT"), tr_noop("ONLINE"), Colors.GOOD) else: - self._connect_status.update("CONNECT", "ERROR", Colors.DANGER) + self._connect_status.update(tr_noop("CONNECT"), tr_noop("ERROR"), Colors.DANGER) def _update_panda_status(self): if ui_state.panda_type == log.PandaState.PandaType.unknown: - self._panda_status.update("NO", "PANDA", Colors.DANGER) + self._panda_status.update(tr_noop("NO"), tr_noop("PANDA"), Colors.DANGER) else: - self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD) + self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD) def _handle_mouse_release(self, mouse_pos: MousePos): if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN): @@ -143,6 +150,9 @@ class Sidebar(Widget): elif rl.check_collision_point_rec(mouse_pos, HOME_BTN) and ui_state.started: if self._on_flag_click: self._on_flag_click() + elif self._recording_audio and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect): + if self._open_settings_callback: + self._open_settings_callback() def _draw_buttons(self, rect: rl.Rectangle): mouse_pos = rl.get_mouse_position() @@ -160,6 +170,17 @@ class Sidebar(Widget): tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint) + # Microphone button + if self._recording_audio: + self._mic_indicator_rect = rl.Rectangle(rect.x + rect.width - 130, rect.y + 245, 75, 40) + + mic_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect) + bg_color = rl.Color(Colors.DANGER.r, Colors.DANGER.g, Colors.DANGER.b, int(255 * 0.65)) if mic_pressed else Colors.DANGER + + rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color) + rl.draw_texture(self._mic_img, int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2), + int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), Colors.WHITE) + def _draw_network_indicator(self, rect: rl.Rectangle): # Signal strength dots x_start = rect.x + 58 @@ -176,7 +197,7 @@ class Sidebar(Widget): # Network type text text_y = rect.y + 247 text_pos = rl.Vector2(rect.x + 58, text_y) - rl.draw_text_ex(self._font_regular, self._net_type, text_pos, FONT_SIZE, 0, Colors.WHITE) + rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE) def _draw_metrics(self, rect: rl.Rectangle): metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)] @@ -189,15 +210,15 @@ class Sidebar(Widget): # Draw colored left edge (clipped rounded rectangle) edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118) rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height)) - rl.draw_rectangle_rounded(edge_rect, 0.18, 10, metric.color) + rl.draw_rectangle_rounded(edge_rect, 0.3, 10, metric.color) rl.end_scissor_mode() # Draw border - rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER) + rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, Colors.METRIC_BORDER) # Draw label and value - labels = [metric.label, metric.value] - text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE) + labels = [tr(metric.label), tr(metric.value)] + text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE * FONT_SCALE) for text in labels: text_size = measure_text_cached(self._font_bold, text, FONT_SIZE) text_y += text_size.y diff --git a/selfdrive/ui/lib/api_helpers.py b/selfdrive/ui/lib/api_helpers.py index b83efedb60..8ed1c22a63 100644 --- a/selfdrive/ui/lib/api_helpers.py +++ b/selfdrive/ui/lib/api_helpers.py @@ -1,12 +1,16 @@ import time from functools import lru_cache from openpilot.common.api import Api +from openpilot.common.time_helpers import system_time_valid TOKEN_EXPIRY_HOURS = 2 @lru_cache(maxsize=1) def _get_token(dongle_id: str, t: int): + if not system_time_valid(): + raise RuntimeError("System time is not valid, cannot generate token") + return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS) diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index be2132c1b7..fc72b4f9c6 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -11,14 +11,14 @@ from openpilot.selfdrive.ui.lib.api_helpers import get_token class PrimeType(IntEnum): - UNKNOWN = -2, - UNPAIRED = -1, - NONE = 0, - MAGENTA = 1, - LITE = 2, - BLUE = 3, - MAGENTA_NEW = 4, - PURPLE = 5, + UNKNOWN = -2 + UNPAIRED = -1 + NONE = 0 + MAGENTA = 1 + LITE = 2 + BLUE = 3 + MAGENTA_NEW = 4 + PURPLE = 5 class PrimeState: @@ -33,7 +33,6 @@ class PrimeState: self._running = False self._thread = None - self.start() def _load_initial_state(self) -> PrimeType: prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType") @@ -96,5 +95,9 @@ class PrimeState: with self._lock: return bool(self.prime_type > PrimeType.NONE) + def is_paired(self) -> bool: + with self._lock: + return self.prime_type > PrimeType.UNPAIRED + def __del__(self): self.stop() diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc deleted file mode 100644 index db96817f1a..0000000000 --- a/selfdrive/ui/main.cc +++ /dev/null @@ -1,36 +0,0 @@ -#include - -#include -#include - -#include "system/hardware/hw.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); - - qInstallMessageHandler(swagLogMessageHandler); - initApp(argc, argv); - - QTranslator translator; - QString translation_file = QString::fromStdString(Params().get("LanguageSetting")); - if (!translator.load(QString(":/%1").arg(translation_file)) && translation_file.length()) { - qCritical() << "Failed to load translation file:" << translation_file; - } - - QApplication a(argc, argv); - a.installTranslator(&translator); - - MainWindow w; - setMainWindow(&w); - a.installEventFilter(&w); - return a.exec(); -} diff --git a/selfdrive/ui/onroad/alert_renderer.py b/selfdrive/ui/onroad/alert_renderer.py index e8817f24b4..a81fbfc440 100644 --- a/selfdrive/ui/onroad/alert_renderer.py +++ b/selfdrive/ui/onroad/alert_renderer.py @@ -4,10 +4,11 @@ from dataclasses import dataclass from cereal import messaging, log from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.hardware import TICI -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import gui_text_box +from openpilot.system.ui.widgets.label import Label AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus @@ -21,14 +22,19 @@ ALERT_FONT_SMALL = 66 ALERT_FONT_MEDIUM = 74 ALERT_FONT_BIG = 88 +ALERT_HEIGHTS = { + AlertSize.small: 271, + AlertSize.mid: 420, +} + SELFDRIVE_STATE_TIMEOUT = 5 # Seconds SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds # Constants ALERT_COLORS = { - AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black - AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange - AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red + AlertStatus.normal: rl.Color(0x15, 0x15, 0x15, 0xF1), # #151515 with alpha 0xF1 + AlertStatus.userPrompt: rl.Color(0xDA, 0x6F, 0x25, 0xF1), # #DA6F25 with alpha 0xF1 + AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 0xF1), # #C92231 with alpha 0xF1 } @@ -42,24 +48,24 @@ class Alert: # Pre-defined alert instances ALERT_STARTUP_PENDING = Alert( - text1="openpilot Unavailable", - text2="Waiting to start", + text1=tr("openpilot Unavailable"), + text2=tr("Waiting to start"), size=AlertSize.mid, status=AlertStatus.normal, ) ALERT_CRITICAL_TIMEOUT = Alert( - text1="TAKE CONTROL IMMEDIATELY", - text2="System Unresponsive", + text1=tr("TAKE CONTROL IMMEDIATELY"), + text2=tr("System Unresponsive"), size=AlertSize.full, status=AlertStatus.critical, ) ALERT_CRITICAL_REBOOT = Alert( - text1="System Unresponsive", - text2="Reboot Device", - size=AlertSize.full, - status=AlertStatus.critical, + text1=tr("System Unresponsive"), + text2=tr("Reboot Device"), + size=AlertSize.mid, + status=AlertStatus.normal, ) @@ -69,14 +75,20 @@ class AlertRenderer(Widget): self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL) self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) + # font size is set dynamically + self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + def get_alert(self, sm: messaging.SubMaster) -> Alert | None: """Generate the current alert based on selfdrive state.""" ss = sm['selfdriveState'] # Check if selfdriveState messages have stopped arriving + recv_frame = sm.recv_frame['selfdriveState'] if not sm.updated['selfdriveState']: - recv_frame = sm.recv_frame['selfdriveState'] - time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS + time_since_onroad = time.monotonic() - ui_state.started_time # 1. Never received selfdriveState since going onroad waiting_for_startup = recv_frame < ui_state.started_frame @@ -95,13 +107,17 @@ class AlertRenderer(Widget): if ss.alertSize == 0: return None + # Don't get old alert + if recv_frame < ui_state.started_frame: + return None + # Return current alert return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw) - def _render(self, rect: rl.Rectangle) -> bool: + def _render(self, rect: rl.Rectangle): alert = self.get_alert(ui_state.sm) if not alert: - return False + return alert_rect = self._get_alert_rect(rect, alert.size) self._draw_background(alert_rect, alert) @@ -113,21 +129,14 @@ class AlertRenderer(Widget): alert_rect.height - 2 * ALERT_PADDING ) self._draw_text(text_rect, alert) - return True def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle: if size == AlertSize.full: return rect - height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == AlertSize.small else - ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING) - - return rl.Rectangle( - rect.x + ALERT_MARGIN, - rect.y + rect.height - ALERT_MARGIN - height, - rect.width - 2 * ALERT_MARGIN, - height - ) + h = ALERT_HEIGHTS.get(size, rect.height) + return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN, + rect.width - ALERT_MARGIN * 2, h - ALERT_MARGIN * 2) def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None: color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal]) @@ -150,13 +159,17 @@ class AlertRenderer(Widget): else: is_long = len(alert.text1) > 15 font_size1 = 132 if is_long else 177 - align_ment = rl.GuiTextAlignment.TEXT_ALIGN_CENTER - vertical_align = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE - text_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height // 2) - gui_text_box(text_rect, alert.text1, font_size1, alignment=align_ment, alignment_vertical=vertical_align, font_weight=FontWeight.BOLD) - text_rect.y = rect.y + rect.height // 2 - gui_text_box(text_rect, alert.text2, ALERT_FONT_BIG, alignment=align_ment) + top_offset = 200 if is_long or '\n' in alert.text1 else 270 + title_rect = rl.Rectangle(rect.x, rect.y + top_offset, rect.width, 600) + self._full_text1_label.set_font_size(font_size1) + self._full_text1_label.set_text(alert.text1) + self._full_text1_label.render(title_rect) + + bottom_offset = 361 if is_long else 420 + subtitle_rect = rl.Rectangle(rect.x, rect.y + rect.height - bottom_offset, rect.width, 300) + self._full_text2_label.set_text(alert.text2) + self._full_text2_label.render(subtitle_rect) def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None: text_size = measure_text_cached(font, text, font_size) diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index dbc1919221..1f202141c3 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -1,9 +1,10 @@ +import time import numpy as np import pyray as rl -from collections.abc import Callable -from cereal import log +from cereal import log, messaging from msgq.visionipc import VisionStreamType -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE +from openpilot.selfdrive.ui import UI_BORDER_SIZE +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer @@ -20,13 +21,14 @@ WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"] BORDER_COLORS = { - UIStatus.DISENGAGED: rl.Color(0x17, 0x33, 0x49, 0xC8), # Blue for disengaged state - UIStatus.OVERRIDE: rl.Color(0x91, 0x9B, 0x95, 0xF1), # Gray for override state - UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state + UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state + UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state + UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state } WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph) ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph) +INF_POINT = np.array([1000.0, 0.0, 0.0]) class AugmentedRoadView(CameraView): @@ -38,9 +40,7 @@ class AugmentedRoadView(CameraView): self.view_from_calib = view_frame_from_device_frame.copy() self.view_from_wide_calib = view_frame_from_device_frame.copy() - self._last_calib_time: float = 0 - self._last_rect_dims = (0.0, 0.0) - self._last_stream_type = stream_type + self._matrix_cache_key = (0, 0.0, 0.0, stream_type) self._cached_matrix: np.ndarray | None = None self._content_rect = rl.Rectangle() @@ -49,14 +49,12 @@ class AugmentedRoadView(CameraView): self.alert_renderer = AlertRenderer() self.driver_state_renderer = DriverStateRenderer() - # Callbacks - self._click_callback: Callable | None = None - - def set_callbacks(self, on_click: Callable | None = None): - self._click_callback = on_click + # debug + self._pm = messaging.PubMaster(['uiDebug']) def _render(self, rect): # Only render when system is started to avoid invalid data access + start_draw = time.monotonic() if not ui_state.started: return @@ -73,9 +71,6 @@ class AugmentedRoadView(CameraView): rect.height - 2 * UI_BORDER_SIZE, ) - # Draw colored border based on driving state - self._draw_border(rect) - # Enable scissor mode to clip all rendering within content rectangle boundaries # This creates a rendering viewport that prevents graphics from drawing outside the border rl.begin_scissor_mode( @@ -91,8 +86,8 @@ class AugmentedRoadView(CameraView): # Draw all UI overlays self.model_renderer.render(self._content_rect) self._hud_renderer.render(self._content_rect) - if not self.alert_renderer.render(self._content_rect): - self.driver_state_renderer.render(self._content_rect) + self.alert_renderer.render(self._content_rect) + self.driver_state_renderer.render(self._content_rect) # Custom UI extension point - add custom overlays here # Use self._content_rect for positioning within camera bounds @@ -100,15 +95,29 @@ class AugmentedRoadView(CameraView): # End clipping region rl.end_scissor_mode() - # Handle click events if no HUD interaction occurred - if not self._hud_renderer.handle_mouse_event(): - if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): - if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect): - self._click_callback() + # Draw colored border based on driving state + self._draw_border(rect) + + # publish uiDebug + msg = messaging.new_message('uiDebug') + msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000 + self._pm.send('uiDebug', msg) + + def _handle_mouse_press(self, _): + if not self._hud_renderer.user_interacting() and self._click_callback is not None: + self._click_callback() + + def _handle_mouse_release(self, _): + # We only call click callback on press if not interacting with HUD + pass def _draw_border(self, rect: rl.Rectangle): + rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK) + border_roundness = 0.12 border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED]) - rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color) + border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE, + rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE) + rl.draw_rectangle_rounded_lines_ex(border_rect, border_roundness, 10, UI_BORDER_SIZE, border_color) def _switch_stream_if_needed(self, sm): if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams: @@ -151,12 +160,13 @@ class AugmentedRoadView(CameraView): def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: # Check if we can use cached matrix - calib_time = ui_state.sm.recv_frame['liveCalibration'] - current_dims = (self._content_rect.width, self._content_rect.height) - if (self._last_calib_time == calib_time and - self._last_rect_dims == current_dims and - self._last_stream_type == self.stream_type and - self._cached_matrix is not None): + cache_key = ( + ui_state.sm.recv_frame['liveCalibration'], + self._content_rect.width, + self._content_rect.height, + self.stream_type + ) + if cache_key == self._matrix_cache_key and self._cached_matrix is not None: return self._cached_matrix # Get camera configuration @@ -167,9 +177,8 @@ class AugmentedRoadView(CameraView): zoom = 2.0 if is_wide_camera else 1.1 # Calculate transforms for vanishing point - inf_point = np.array([1000.0, 0.0, 0.0]) calib_transform = intrinsic @ calibration - kep = calib_transform @ inf_point + kep = calib_transform @ INF_POINT # Calculate center points and dimensions x, y = self._content_rect.x, self._content_rect.y @@ -192,9 +201,7 @@ class AugmentedRoadView(CameraView): x_offset, y_offset = 0, 0 # Cache the computed transformation matrix to avoid recalculations - self._last_calib_time = calib_time - self._last_rect_dims = current_dims - self._last_stream_type = self.stream_type + self._matrix_cache_key = cache_key self._cached_matrix = np.array([ [zoom * 2 * cx / w, 0, -x_offset / w * 2], [0, zoom * 2 * cy / h, -y_offset / h * 2], diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index ca031e2f17..87db7cc636 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -8,6 +8,7 @@ from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.widgets import Widget +from openpilot.selfdrive.ui.ui_state import ui_state CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts @@ -67,6 +68,7 @@ else: class CameraView(Widget): def __init__(self, name: str, stream_type: VisionStreamType): super().__init__() + # TODO: implement a receiver and connect thread self._name = name # Primary stream self.client = VisionIpcClient(name, stream_type, conflate=True) @@ -103,6 +105,20 @@ class CameraView(Widget): self.egl_texture = rl.load_texture_from_image(temp_image) rl.unload_image(temp_image) + ui_state.add_offroad_transition_callback(self._offroad_transition) + + def _offroad_transition(self): + # Reconnect if not first time going onroad + if ui_state.is_onroad() and self.frame is not None: + # Prevent old frames from showing when going onroad. Qt has a separate thread + # which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough + # and only clears internal buffers, not the message queue. + self.frame = None + self.available_streams.clear() + if self.client: + del self.client + self.client = VisionIpcClient(self._name, self._stream_type, conflate=True) + def _set_placeholder_color(self, color: rl.Color): """Set a placeholder color to be drawn when no frame is available.""" self._placeholder_color = color @@ -139,6 +155,8 @@ class CameraView(Widget): if self.shader and self.shader.id: rl.unload_shader(self.shader) + self.frame = None + self.available_streams.clear() self.client = None def __del__(self): @@ -175,6 +193,9 @@ class CameraView(Widget): if buffer: self._texture_needs_update = True self.frame = buffer + elif not self.client.is_connected(): + # ensure we clear the displayed frame when the connection is lost + self.frame = None if not self.frame: self._draw_placeholder(rect) diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index c8b4e62030..543ea35e81 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -3,8 +3,9 @@ import pyray as rl from msgq.visionipc import VisionStreamType from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets.label import gui_label @@ -12,17 +13,25 @@ class DriverCameraDialog(CameraView): def __init__(self): super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER) self.driver_state_renderer = DriverStateRenderer() + # TODO: this can grow unbounded, should be given some thought + device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld) + ui_state.params.put_bool("IsDriverViewEnabled", True) + + def stop_dmonitoringmodeld(self): + ui_state.params.put_bool("IsDriverViewEnabled", False) + gui_app.set_modal_overlay(None) + + def _handle_mouse_release(self, _): + super()._handle_mouse_release(_) + self.stop_dmonitoringmodeld() def _render(self, rect): super()._render(rect) - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): - return 1 - if not self.frame: gui_label( rect, - "camera starting", + tr("camera starting"), font_size=100, font_weight=FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, diff --git a/selfdrive/ui/onroad/driver_state.py b/selfdrive/ui/onroad/driver_state.py index a25d9bd316..7b3181d1ac 100644 --- a/selfdrive/ui/onroad/driver_state.py +++ b/selfdrive/ui/onroad/driver_state.py @@ -1,10 +1,14 @@ import numpy as np import pyray as rl +from cereal import log from dataclasses import dataclass -from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE +from openpilot.selfdrive.ui import UI_BORDER_SIZE +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget +AlertSize = log.SelfdriveState.AlertSize + # Default 3D coordinates for face keypoints as a NumPy array DEFAULT_FACE_KPTS_3D = np.array([ [-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00], @@ -50,7 +54,6 @@ class DriverStateRenderer(Widget): self.is_active = False self.is_rhd = False self.dm_fade_state = 0.0 - self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self.driver_pose_vals = np.zeros(3, dtype=np.float32) self.driver_pose_diff = np.zeros(3, dtype=np.float32) self.driver_pose_sins = np.zeros(3, dtype=np.float32) @@ -75,8 +78,8 @@ class DriverStateRenderer(Widget): self.engaged_color = rl.Color(26, 242, 66, 255) self.disengaged_color = rl.Color(139, 139, 139, 255) - self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and - ui_state.sm.seen['driverMonitoringState'])) + self.set_visible(lambda: (ui_state.sm["selfdriveState"].alertSize == AlertSize.none and + ui_state.sm.recv_frame["driverStateV2"] > ui_state.started_frame)) def _render(self, rect): # Set opacity based on active state @@ -106,11 +109,7 @@ class DriverStateRenderer(Widget): def _update_state(self): """Update the driver monitoring state based on model data""" sm = ui_state.sm - if not sm.updated["driverMonitoringState"]: - if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or - self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height): - self._pre_calculate_drawing_elements() - self.last_rect = self._rect + if not self.is_visible: return # Get monitoring state @@ -222,7 +221,7 @@ class DriverStateRenderer(Widget): radius_y = arc_data.height / 2 x_coords = center_x + np.cos(angles) * radius_x - y_coords = center_y + np.sin(angles) * radius_y + y_coords = center_y - np.sin(angles) * radius_y arc_lines = self.h_arc_lines if is_horizontal else self.v_arc_lines for i, (x_coord, y_coord) in enumerate(zip(x_coords, y_coords, strict=True)): diff --git a/selfdrive/ui/onroad/exp_button.py b/selfdrive/ui/onroad/exp_button.py index 27f5763077..e5d8171413 100644 --- a/selfdrive/ui/onroad/exp_button.py +++ b/selfdrive/ui/onroad/exp_button.py @@ -32,26 +32,21 @@ class ExpButton(Widget): self._experimental_mode = selfdrive_state.experimentalMode self._engageable = selfdrive_state.engageable or selfdrive_state.enabled - def handle_mouse_event(self) -> bool: - if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect): - if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and - self._is_toggle_allowed()): - new_mode = not self._experimental_mode - self._params.put_bool("ExperimentalMode", new_mode) + def _handle_mouse_release(self, _): + super()._handle_mouse_release(_) + if self._is_toggle_allowed(): + new_mode = not self._experimental_mode + self._params.put_bool("ExperimentalMode", new_mode) - # Hold new state temporarily - self._held_mode = new_mode - self._hold_end_time = time.monotonic() + self._hold_duration - return True - return False + # Hold new state temporarily + self._held_mode = new_mode + self._hold_end_time = time.monotonic() + self._hold_duration def _render(self, rect: rl.Rectangle) -> None: center_x = int(self._rect.x + self._rect.width // 2) center_y = int(self._rect.y + self._rect.height // 2) - mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect) - mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed - self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255 + self._white_color.a = 180 if self.is_pressed or not self._engageable else 255 texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg) @@ -71,8 +66,5 @@ class ExpButton(Widget): if not self._params.get_bool("ExperimentalModeConfirmed"): return False - car_params = ui_state.sm["carParams"] - if car_params.alphaLongitudinalAvailable: - return self._params.get_bool("AlphaLongitudinalEnabled") - else: - return car_params.openpilotLongitudinalControl + # Mirror exp mode toggle using persistent car params + return ui_state.has_longitudinal_control diff --git a/selfdrive/ui/onroad/hud_renderer.py b/selfdrive/ui/onroad/hud_renderer.py index 536d993389..a2459c27e2 100644 --- a/selfdrive/ui/onroad/hud_renderer.py +++ b/selfdrive/ui/onroad/hud_renderer.py @@ -4,6 +4,7 @@ from openpilot.common.constants import CV from openpilot.selfdrive.ui.onroad.exp_button import ExpButton from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -60,7 +61,7 @@ class HudRenderer(Widget): super().__init__() """Initialize the HUD renderer.""" self.is_cruise_set: bool = False - self.is_cruise_available: bool = False + self.is_cruise_available: bool = True self.set_speed: float = SET_SPEED_NA self.speed: float = 0.0 self.v_ego_cluster_seen: bool = False @@ -69,7 +70,7 @@ class HudRenderer(Widget): self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM) - self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size) + self._exp_button: ExpButton = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size) def _update_state(self) -> None: """Update HUD state based on car state and controls state.""" @@ -120,8 +121,8 @@ class HudRenderer(Widget): button_y = rect.y + UI_CONFIG.border_size self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size)) - def handle_mouse_event(self) -> bool: - return bool(self._exp_button.handle_mouse_event()) + def user_interacting(self) -> bool: + return self._exp_button.is_pressed def _draw_set_speed(self, rect: rl.Rectangle) -> None: """Draw the MAX speed indicator box.""" @@ -130,8 +131,8 @@ class HudRenderer(Widget): y = rect.y + 45 set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height) - rl.draw_rectangle_rounded(set_speed_rect, 0.2, 30, COLORS.black_translucent) - rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.2, 30, 6, COLORS.border_translucent) + rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.black_translucent) + rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.border_translucent) max_color = COLORS.grey set_speed_color = COLORS.dark_grey @@ -144,7 +145,7 @@ class HudRenderer(Widget): elif ui_state.status == UIStatus.OVERRIDE: max_color = COLORS.override - max_text = "MAX" + max_text = tr("MAX") max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x rl.draw_text_ex( self._font_semi_bold, @@ -173,7 +174,7 @@ class HudRenderer(Widget): speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) - unit_text = "km/h" if ui_state.is_metric else "mph" + unit_text = tr("km/h") if ui_state.is_metric else tr("mph") unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent) diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index 932773755d..b9f601f8fb 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -3,20 +3,17 @@ import numpy as np import pyray as rl from cereal import messaging, car from dataclasses import dataclass, field +from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import DEFAULT_FPS -from openpilot.system.ui.lib.shader_polygon import draw_polygon +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from openpilot.system.ui.widgets import Widget CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 MAX_DRAW_DISTANCE = 100.0 -PATH_COLOR_TRANSITION_DURATION = 0.5 # Seconds for color transition animation -PATH_BLEND_INCREMENT = 1.0 / (PATH_COLOR_TRANSITION_DURATION * DEFAULT_FPS) - -MAX_POINTS = 200 THROTTLE_COLORS = [ rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4) @@ -49,7 +46,7 @@ class ModelRenderer(Widget): super().__init__() self._longitudinal_control = False self._experimental_mode = False - self._blend_factor = 1.0 + self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps) self._prev_allow_throttle = True self._lane_line_probs = np.zeros(4, dtype=np.float32) self._road_edge_stds = np.zeros(2, dtype=np.float32) @@ -67,12 +64,12 @@ class ModelRenderer(Widget): self._transform_dirty = True self._clip_region = None - self._exp_gradient = { - 'start': (0.0, 1.0), # Bottom of path - 'end': (0.0, 0.0), # Top of path - 'colors': [], - 'stops': [], - } + self._exp_gradient = Gradient( + start=(0.0, 1.0), # Bottom of path + end=(0.0, 0.0), # Top of path + colors=[], + stops=[], + ) # Get longitudinal control setting from car parameters if car_params := Params().get("CarParams"): @@ -170,12 +167,12 @@ class ModelRenderer(Widget): # Update lane lines using raw points for i, lane_line in enumerate(self._lane_lines): lane_line.projected_points = self._map_line_to_polygon( - lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx + lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx, max_distance ) # Update road edges using raw points for road_edge in self._road_edges: - road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx) + road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance) # Update path using raw points if lead and lead.status: @@ -184,7 +181,7 @@ class ModelRenderer(Widget): max_idx = self._get_path_length_idx(path_x_array, max_distance) self._path.projected_points = self._map_line_to_polygon( - self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False + self._path.raw_points, 0.9, self._path_offset_z, max_idx, max_distance, allow_invert=False ) self._update_experimental_gradient() @@ -227,8 +224,12 @@ class ModelRenderer(Widget): i += 1 + (1 if (i + 2) < max_len else 0) # Store the gradient in the path object - self._exp_gradient['colors'] = segment_colors - self._exp_gradient['stops'] = gradient_stops + self._exp_gradient = Gradient( + start=(0.0, 1.0), # Bottom of path + end=(0.0, 0.0), # Top of path + colors=segment_colors, + stops=gradient_stops, + ) def _update_lead_vehicle(self, d_rel, v_rel, point, rect): speed_buff, lead_buff = 10.0, 40.0 @@ -277,36 +278,25 @@ class ModelRenderer(Widget): if not self._path.projected_points.size: return + allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control + self._blend_filter.update(int(allow_throttle)) + if self._experimental_mode: # Draw with acceleration coloring - if len(self._exp_gradient['colors']) > 1: + if len(self._exp_gradient.colors) > 1: draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient) else: draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30)) else: - # Draw with throttle/no throttle gradient - allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control - - # Start transition if throttle state changes - if allow_throttle != self._prev_allow_throttle: - self._prev_allow_throttle = allow_throttle - self._blend_factor = max(1.0 - self._blend_factor, 0.0) - - # Update blend factor - if self._blend_factor < 1.0: - self._blend_factor = min(self._blend_factor + PATH_BLEND_INCREMENT, 1.0) - - begin_colors = NO_THROTTLE_COLORS if allow_throttle else THROTTLE_COLORS - end_colors = THROTTLE_COLORS if allow_throttle else NO_THROTTLE_COLORS - - # Blend colors based on transition - blended_colors = self._blend_colors(begin_colors, end_colors, self._blend_factor) - gradient = { - 'start': (0.0, 1.0), # Bottom of path - 'end': (0.0, 0.0), # Top of path - 'colors': blended_colors, - 'stops': [0.0, 0.5, 1.0], - } + # Blend throttle/no throttle colors based on transition + blend_factor = round(self._blend_filter.x * 100) / 100 + blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor) + gradient = Gradient( + start=(0.0, 1.0), # Bottom of path + end=(0.0, 0.0), # Top of path + colors=blended_colors, + stops=[0.0, 0.5, 1.0], + ) draw_polygon(self._rect, self._path.projected_points, gradient=gradient) def _draw_lead_indicator(self): @@ -319,11 +309,11 @@ class ModelRenderer(Widget): rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha)) @staticmethod - def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int: - """Get the index corresponding to the given path height""" + def _get_path_length_idx(pos_x_array: np.ndarray, path_distance: float) -> int: + """Get the index corresponding to the given path distance""" if len(pos_x_array) == 0: return 0 - indices = np.where(pos_x_array <= path_height)[0] + indices = np.where(pos_x_array <= path_distance)[0] return indices[-1] if indices.size > 0 else 0 def _map_to_screen(self, in_x, in_y, in_z): @@ -342,13 +332,24 @@ class ModelRenderer(Widget): return (x, y) - def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, allow_invert: bool = True) -> np.ndarray: + def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, max_distance: float, allow_invert: bool = True) -> np.ndarray: """Convert 3D line to 2D polygon for rendering.""" if line.shape[0] == 0: return np.empty((0, 2), dtype=np.float32) # Slice points and filter non-negative x-coordinates points = line[:max_idx + 1] + + # Interpolate around max_idx so path end is smooth (max_distance is always >= p0.x) + if 0 < max_idx < line.shape[0] - 1: + p0 = line[max_idx] + p1 = line[max_idx + 1] + x0, x1 = p0[0], p1[0] + interp_y = np.interp(max_distance, [x0, x1], [p0[1], p1[1]]) + interp_z = np.interp(max_distance, [x0, x1], [p0[2], p1[2]]) + interp_point = np.array([max_distance, interp_y, interp_z], dtype=points.dtype) + points = np.concatenate((points, interp_point[None, :]), axis=0) + points = points[points[:, 0] >= 0] if points.shape[0] == 0: return np.empty((0, 2), dtype=np.float32) diff --git a/selfdrive/ui/qt/body.cc b/selfdrive/ui/qt/body.cc deleted file mode 100644 index e01adbe063..0000000000 --- a/selfdrive/ui/qt/body.cc +++ /dev/null @@ -1,161 +0,0 @@ -#include "selfdrive/ui/qt/body.h" - -#include -#include - -#include -#include - -#include "common/params.h" -#include "common/timing.h" - -RecordButton::RecordButton(QWidget *parent) : QPushButton(parent) { - setCheckable(true); - setChecked(false); - setFixedSize(148, 148); - - QObject::connect(this, &QPushButton::toggled, [=]() { - setEnabled(false); - }); -} - -void RecordButton::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QPoint center(width() / 2, height() / 2); - - QColor bg(isChecked() ? "#FFFFFF" : "#737373"); - QColor accent(isChecked() ? "#FF0000" : "#FFFFFF"); - if (!isEnabled()) { - bg = QColor("#404040"); - accent = QColor("#FFFFFF"); - } - - if (isDown()) { - accent.setAlphaF(0.7); - } - - p.setPen(Qt::NoPen); - p.setBrush(bg); - p.drawEllipse(center, 74, 74); - - p.setPen(QPen(accent, 6)); - p.setBrush(Qt::NoBrush); - p.drawEllipse(center, 42, 42); - - p.setPen(Qt::NoPen); - p.setBrush(accent); - p.drawEllipse(center, 22, 22); -} - - -BodyWindow::BodyWindow(QWidget *parent) : fuel_filter(1.0, 5., 1. / UI_FREQ), QWidget(parent) { - QStackedLayout *layout = new QStackedLayout(this); - layout->setStackingMode(QStackedLayout::StackAll); - - QWidget *w = new QWidget; - QVBoxLayout *vlayout = new QVBoxLayout(w); - vlayout->setMargin(45); - layout->addWidget(w); - - // face - face = new QLabel(); - face->setAlignment(Qt::AlignCenter); - layout->addWidget(face); - awake = new QMovie("../assets/body/awake.gif", {}, this); - awake->setCacheMode(QMovie::CacheAll); - sleep = new QMovie("../assets/body/sleep.gif", {}, this); - sleep->setCacheMode(QMovie::CacheAll); - - // record button - btn = new RecordButton(this); - vlayout->addWidget(btn, 0, Qt::AlignBottom | Qt::AlignRight); - QObject::connect(btn, &QPushButton::clicked, [=](bool checked) { - btn->setEnabled(false); - Params().putBool("DisableLogging", !checked); - last_button = nanos_since_boot(); - }); - w->raise(); - - QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState); -} - -void BodyWindow::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - p.fillRect(rect(), QColor(0, 0, 0)); - - // battery outline + detail - p.translate(width() - 136, 16); - const QColor gray = QColor("#737373"); - p.setBrush(Qt::NoBrush); - p.setPen(QPen(gray, 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); - p.drawRoundedRect(2, 2, 78, 36, 8, 8); - - p.setPen(Qt::NoPen); - p.setBrush(gray); - p.drawRoundedRect(84, 12, 6, 16, 4, 4); - p.drawRect(84, 12, 3, 16); - - // battery level - double fuel = std::clamp(fuel_filter.x(), 0.2f, 1.0f); - const int m = 5; // manual margin since we can't do an inner border - p.setPen(Qt::NoPen); - p.setBrush(fuel > 0.25 ? QColor("#32D74B") : QColor("#FF453A")); - p.drawRoundedRect(2 + m, 2 + m, (78 - 2*m)*fuel, 36 - 2*m, 4, 4); - - // charging status - if (charging) { - p.setPen(Qt::NoPen); - p.setBrush(Qt::white); - const QPolygonF charger({ - QPointF(12.31, 0), - QPointF(12.31, 16.92), - QPointF(18.46, 16.92), - QPointF(6.15, 40), - QPointF(6.15, 23.08), - QPointF(0, 23.08), - }); - p.drawPolygon(charger.translated(98, 0)); - } -} - -void BodyWindow::offroadTransition(bool offroad) { - btn->setChecked(true); - btn->setEnabled(true); - fuel_filter.reset(1.0); -} - -void BodyWindow::updateState(const UIState &s) { - if (!isVisible()) { - return; - } - - const SubMaster &sm = *(s.sm); - auto cs = sm["carState"].getCarState(); - - charging = cs.getCharging(); - fuel_filter.update(cs.getFuelGauge()); - - // TODO: use carState.standstill when that's fixed - const bool standstill = std::abs(cs.getVEgo()) < 0.01; - QMovie *m = standstill ? sleep : awake; - if (m != face->movie()) { - face->setMovie(m); - face->movie()->start(); - } - - // update record button state - if (sm.updated("managerState") && (sm.rcv_time("managerState") - last_button)*1e-9 > 0.5) { - for (auto proc : sm["managerState"].getManagerState().getProcesses()) { - if (proc.getName() == "loggerd") { - btn->setEnabled(true); - btn->setChecked(proc.getRunning()); - } - } - } - - update(); -} diff --git a/selfdrive/ui/qt/body.h b/selfdrive/ui/qt/body.h deleted file mode 100644 index 187e015af7..0000000000 --- a/selfdrive/ui/qt/body.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#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 - -public: - RecordButton(QWidget* parent = 0); - -private: - void paintEvent(QPaintEvent*) override; -}; - -class BodyWindow : public QWidget { - Q_OBJECT - -public: - BodyWindow(QWidget* parent = 0); - -private: - bool charging = false; - uint64_t last_button = 0; - FirstOrderFilter fuel_filter; - QLabel *face; - QMovie *awake, *sleep; - RecordButton *btn; - void paintEvent(QPaintEvent*) override; - -private slots: - void updateState(const UIState &s); - void offroadTransition(bool onroad); -}; diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc deleted file mode 100644 index 3f7457192e..0000000000 --- a/selfdrive/ui/qt/home.cc +++ /dev/null @@ -1,95 +0,0 @@ -#include "selfdrive/ui/qt/home.h" - -#include -#include - -#include "selfdrive/ui/qt/util.h" - -// HomeWindow: the container for the offroad and onroad UIs - -HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { - QHBoxLayout *main_layout = new QHBoxLayout(this); - main_layout->setMargin(0); - main_layout->setSpacing(0); - - sidebar = new Sidebar(this); - main_layout->addWidget(sidebar); - QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings); - - slayout = new QStackedLayout(); - main_layout->addLayout(slayout); - - home = new OffroadHome(this); - QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings); - slayout->addWidget(home); - - onroad = new OnroadWindow(this); - slayout->addWidget(onroad); - - body = new BodyWindow(this); - slayout->addWidget(body); - - driver_view = new DriverViewWindow(this); - connect(driver_view, &DriverViewWindow::done, [=] { - showDriverView(false); - }); - slayout->addWidget(driver_view); - setAttribute(Qt::WA_NoSystemBackground); - QObject::connect(uiState(), &UIState::uiUpdate, this, &HomeWindow::updateState); - QObject::connect(uiState(), &UIState::offroadTransition, this, &HomeWindow::offroadTransition); - QObject::connect(uiState(), &UIState::offroadTransition, sidebar, &Sidebar::offroadTransition); -} - -void HomeWindow::showSidebar(bool show) { - sidebar->setVisible(show); -} - -void HomeWindow::updateState(const UIState &s) { - const SubMaster &sm = *(s.sm); - - // switch to the generic robot UI - if (onroad->isVisible() && !body->isEnabled() && sm["carParams"].getCarParams().getNotCar()) { - body->setEnabled(true); - slayout->setCurrentWidget(body); - } -} - -void HomeWindow::offroadTransition(bool offroad) { - body->setEnabled(false); - sidebar->setVisible(offroad); - if (offroad) { - slayout->setCurrentWidget(home); - } else { - slayout->setCurrentWidget(onroad); - } -} - -void HomeWindow::showDriverView(bool show) { - if (show) { - emit closeSettings(); - slayout->setCurrentWidget(driver_view); - } else { - slayout->setCurrentWidget(home); - } - sidebar->setVisible(show == false); -} - -void HomeWindow::mousePressEvent(QMouseEvent* e) { - // Handle sidebar collapsing - if ((onroad->isVisible() || body->isVisible()) && (!sidebar->isVisible() || e->x() > sidebar->width())) { - sidebar->setVisible(!sidebar->isVisible()); - } -} - -void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { - HomeWindow::mousePressEvent(e); - const SubMaster &sm = *(uiState()->sm); - if (sm["carParams"].getCarParams().getNotCar()) { - if (onroad->isVisible()) { - slayout->setCurrentWidget(body); - } else if (body->isVisible()) { - slayout->setCurrentWidget(onroad); - } - showSidebar(false); - } -} diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h deleted file mode 100644 index b333624163..0000000000 --- a/selfdrive/ui/qt/home.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "selfdrive/ui/ui.h" -#include "selfdrive/ui/qt/offroad/driverview.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 -#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" -#endif - -#include "selfdrive/ui/qt/offroad/offroad_home.h" - -class HomeWindow : public QWidget { - Q_OBJECT - -public: - explicit HomeWindow(QWidget* parent = 0); - -signals: - void openSettings(int index = 0, const QString ¶m = ""); - void closeSettings(); - -public slots: - void offroadTransition(bool offroad); - void showDriverView(bool show); - void showSidebar(bool show); - -protected: - void mousePressEvent(QMouseEvent* e) override; - void mouseDoubleClickEvent(QMouseEvent* e) override; - - Sidebar *sidebar; - OffroadHome *home; - OnroadWindow *onroad; - BodyWindow *body; - DriverViewWindow *driver_view; - QStackedLayout *slayout; - -protected slots: - virtual void updateState(const UIState &s); -}; diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc deleted file mode 100644 index 7754e6e5ba..0000000000 --- a/selfdrive/ui/qt/network/networking.cc +++ /dev/null @@ -1,420 +0,0 @@ -#include "selfdrive/ui/qt/network/networking.h" - -#include - -#include -#include -#include - -#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; - -// Networking functions - -Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { - main_layout = new QStackedLayout(this); - - wifi = new WifiManager(this); - connect(wifi, &WifiManager::refreshSignal, this, &Networking::refresh); - connect(wifi, &WifiManager::wrongPassword, this, &Networking::wrongPassword); - - wifiScreen = new QWidget(this); - QVBoxLayout* vlayout = new QVBoxLayout(wifiScreen); - vlayout->setContentsMargins(20, 20, 20, 20); - if (show_advanced) { - QPushButton* advancedSettings = new QPushButton(tr("Advanced")); - advancedSettings->setObjectName("advanced_btn"); - advancedSettings->setStyleSheet("margin-right: 30px;"); - advancedSettings->setFixedSize(400, 100); - connect(advancedSettings, &QPushButton::clicked, [=]() { main_layout->setCurrentWidget(an); }); - vlayout->addSpacing(10); - vlayout->addWidget(advancedSettings, 0, Qt::AlignRight); - vlayout->addSpacing(10); - } - - wifiWidget = new WifiUI(this, wifi); - wifiWidget->setObjectName("wifiWidget"); - connect(wifiWidget, &WifiUI::connectToNetwork, this, &Networking::connectToNetwork); - - ScrollView *wifiScroller = new ScrollView(wifiWidget, this); - wifiScroller->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - vlayout->addWidget(wifiScroller, 1); - main_layout->addWidget(wifiScreen); - - an = new AdvancedNetworking(this, wifi); - connect(an, &AdvancedNetworking::backPress, [=]() { main_layout->setCurrentWidget(wifiScreen); }); - connect(an, &AdvancedNetworking::requestWifiScreen, [=]() { main_layout->setCurrentWidget(wifiScreen); }); - main_layout->addWidget(an); - - QPalette pal = palette(); - pal.setColor(QPalette::Window, QColor(0x29, 0x29, 0x29)); - setAutoFillBackground(true); - setPalette(pal); - - setStyleSheet(R"( - #wifiWidget > QPushButton, #back_btn, #advanced_btn { - font-size: 50px; - margin: 0px; - padding: 15px; - border-width: 0; - border-radius: 30px; - color: #dddddd; - background-color: #393939; - } - #back_btn:pressed, #advanced_btn:pressed { - background-color: #4a4a4a; - } - )"); - main_layout->setCurrentWidget(wifiScreen); -} - -void Networking::setPrimeType(PrimeState::Type type) { - 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); -} - -void Networking::refresh() { - wifiWidget->refresh(); - an->refresh(); -} - -void Networking::connectToNetwork(const Network n) { - if (wifi->isKnownConnection(n.ssid)) { - wifi->activateWifiConnection(n.ssid); - } else if (n.security_type == SecurityType::OPEN) { - wifi->connect(n, false); - } else if (n.security_type == SecurityType::WPA) { - QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); - if (!pass.isEmpty()) { - wifi->connect(n, false, pass); - } - } -} - -void Networking::wrongPassword(const QString &ssid) { - if (wifi->seenNetworks.contains(ssid)) { - const Network &n = wifi->seenNetworks.value(ssid); - QString pass = InputDialog::getText(tr("Wrong password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); - if (!pass.isEmpty()) { - wifi->connect(n, false, pass); - } - } -} - -void Networking::showEvent(QShowEvent *event) { - wifi->start(); -} - -void Networking::hideEvent(QHideEvent *event) { - main_layout->setCurrentWidget(wifiScreen); - wifi->stop(); -} - -// AdvancedNetworking functions - -AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWidget(parent), wifi(wifi) { - - QVBoxLayout* main_layout = new QVBoxLayout(this); - main_layout->setMargin(40); - main_layout->setSpacing(20); - - // Back button - QPushButton* back = new QPushButton(tr("Back")); - back->setObjectName("back_btn"); - back->setFixedSize(400, 100); - connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); - main_layout->addWidget(back, 0, Qt::AlignLeft); - - ListWidget *list = new ListWidget(this); - // Enable tethering layout - tetheringToggle = new ToggleControl(tr("Enable Tethering"), "", "", wifi->isTetheringEnabled()); - list->addItem(tetheringToggle); - QObject::connect(tetheringToggle, &ToggleControl::toggleFlipped, this, &AdvancedNetworking::toggleTethering); - - // Change tethering password - ButtonControl *editPasswordButton = new ButtonControl(tr("Tethering Password"), tr("EDIT")); - connect(editPasswordButton, &ButtonControl::clicked, [=]() { - QString pass = InputDialog::getText(tr("Enter new tethering password"), this, "", true, 8, wifi->getTetheringPassword()); - if (!pass.isEmpty()) { - wifi->changeTetheringPassword(pass); - } - }); - list->addItem(editPasswordButton); - - // IP address - ipLabel = new LabelControl(tr("IP Address"), wifi->ipv4_address); - list->addItem(ipLabel); - - // Roaming toggle - const bool roamingEnabled = params.getBool("GsmRoaming"); - roamingToggle = new ToggleControl(tr("Enable Roaming"), "", "", roamingEnabled); - QObject::connect(roamingToggle, &ToggleControl::toggleFlipped, [=](bool state) { - params.putBool("GsmRoaming", state); - wifi->updateGsmSettings(state, QString::fromStdString(params.get("GsmApn")), params.getBool("GsmMetered")); - }); - list->addItem(roamingToggle); - - // APN settings - editApnButton = new ButtonControl(tr("APN Setting"), tr("EDIT")); - connect(editApnButton, &ButtonControl::clicked, [=]() { - const QString cur_apn = QString::fromStdString(params.get("GsmApn")); - QString apn = InputDialog::getText(tr("Enter APN"), this, tr("leave blank for automatic configuration"), false, -1, cur_apn).trimmed(); - - if (apn.isEmpty()) { - params.remove("GsmApn"); - } else { - params.put("GsmApn", apn.toStdString()); - } - wifi->updateGsmSettings(params.getBool("GsmRoaming"), apn, params.getBool("GsmMetered")); - }); - list->addItem(editApnButton); - - // Cellular metered toggle (prime lite or none) - const bool metered = params.getBool("GsmMetered"); - cellularMeteredToggle = new ToggleControl(tr("Cellular Metered"), tr("Prevent large data uploads when on a metered cellular connection"), "", metered); - QObject::connect(cellularMeteredToggle, &SshToggle::toggleFlipped, [=](bool state) { - params.putBool("GsmMetered", state); - wifi->updateGsmSettings(params.getBool("GsmRoaming"), QString::fromStdString(params.get("GsmApn")), state); - }); - list->addItem(cellularMeteredToggle); - - // Wi-Fi metered toggle - std::vector metered_button_texts{tr("default"), tr("metered"), tr("unmetered")}; - wifiMeteredToggle = new MultiButtonControl(tr("Wi-Fi Network Metered"), tr("Prevent large data uploads when on a metered Wi-Fi connection"), "", metered_button_texts); - QObject::connect(wifiMeteredToggle, &MultiButtonControl::buttonClicked, [=](int id) { - wifiMeteredToggle->setEnabled(false); - MeteredType metered = MeteredType::UNKNOWN; - if (id == NM_METERED_YES) { - metered = MeteredType::YES; - } else if (id == NM_METERED_NO) { - metered = MeteredType::NO; - } - auto pending_call = wifi->setCurrentNetworkMetered(metered); - if (pending_call) { - QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(*pending_call); - QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=]() { - refresh(); - watcher->deleteLater(); - }); - } - }); - list->addItem(wifiMeteredToggle); - - // Hidden Network - hiddenNetworkButton = new ButtonControl(tr("Hidden Network"), tr("CONNECT")); - connect(hiddenNetworkButton, &ButtonControl::clicked, [=]() { - QString ssid = InputDialog::getText(tr("Enter SSID"), this, "", false, 1); - if (!ssid.isEmpty()) { - QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(ssid), true, -1); - Network hidden_network; - hidden_network.ssid = ssid.toUtf8(); - if (!pass.isEmpty()) { - hidden_network.security_type = SecurityType::WPA; - wifi->connect(hidden_network, true, pass); - } else { - wifi->connect(hidden_network, true); - } - emit requestWifiScreen(); - } - }); - list->addItem(hiddenNetworkButton); - - // Set initial config - wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn")), metered); - - main_layout->addWidget(new ScrollView(list, this)); - main_layout->addStretch(1); -} - -void AdvancedNetworking::setGsmVisible(bool visible) { - roamingToggle->setVisible(visible); - editApnButton->setVisible(visible); - cellularMeteredToggle->setVisible(visible); -} - -void AdvancedNetworking::refresh() { - ipLabel->setText(wifi->ipv4_address); - tetheringToggle->setEnabled(true); - - if (wifi->isTetheringEnabled() || wifi->ipv4_address == "") { - wifiMeteredToggle->setEnabled(false); - wifiMeteredToggle->setCheckedButton(0); - } else if (wifi->ipv4_address != "") { - MeteredType metered = wifi->currentNetworkMetered(); - wifiMeteredToggle->setEnabled(true); - wifiMeteredToggle->setCheckedButton(static_cast(metered)); - } - - update(); -} - -void AdvancedNetworking::toggleTethering(bool enabled) { - wifi->setTetheringEnabled(enabled); - tetheringToggle->setEnabled(false); - if (enabled) { - wifiMeteredToggle->setEnabled(false); - wifiMeteredToggle->setCheckedButton(0); - } -} - -// WifiUI functions - -WifiUI::WifiUI(QWidget *parent, WifiManager* wifi) : QWidget(parent), wifi(wifi) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); - - // load imgs - for (const auto &s : {"low", "medium", "high", "full"}) { - QPixmap pix(ASSET_PATH + "/icons/wifi_strength_" + s + ".svg"); - strengths.push_back(pix.scaledToHeight(68, Qt::SmoothTransformation)); - } - lock = QPixmap(ASSET_PATH + "icons/lock_closed.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); - checkmark = QPixmap(ASSET_PATH + "icons/checkmark.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); - circled_slash = QPixmap(ASSET_PATH + "icons/circled_slash.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); - - scanningLabel = new QLabel(tr("Scanning for networks...")); - scanningLabel->setStyleSheet("font-size: 65px;"); - main_layout->addWidget(scanningLabel, 0, Qt::AlignCenter); - - wifi_list_widget = new ListWidget(this); - wifi_list_widget->setVisible(false); - main_layout->addWidget(wifi_list_widget); - - setStyleSheet(R"( - QScrollBar::handle:vertical { - min-height: 0px; - border-radius: 4px; - background-color: #8A8A8A; - } - #forgetBtn { - font-size: 32px; - font-weight: 600; - color: #292929; - background-color: #BDBDBD; - border-width: 1px solid #828282; - border-radius: 5px; - padding: 40px; - padding-bottom: 16px; - padding-top: 16px; - } - #forgetBtn:pressed { - background-color: #828282; - } - #connecting { - font-size: 32px; - font-weight: 600; - color: white; - border-radius: 0; - padding: 27px; - padding-left: 43px; - padding-right: 43px; - background-color: black; - } - #ssidLabel { - text-align: left; - border: none; - padding-top: 50px; - padding-bottom: 50px; - } - #ssidLabel:disabled { - color: #696969; - } - )"); -} - -void WifiUI::refresh() { - bool is_empty = wifi->seenNetworks.isEmpty(); - scanningLabel->setVisible(is_empty); - wifi_list_widget->setVisible(!is_empty); - if (is_empty) return; - - setUpdatesEnabled(false); - - const bool is_tethering_enabled = wifi->isTetheringEnabled(); - QList sortedNetworks = wifi->seenNetworks.values(); - std::sort(sortedNetworks.begin(), sortedNetworks.end(), compare_by_strength); - - int n = 0; - for (Network &network : sortedNetworks) { - QPixmap status_icon; - if (network.connected == ConnectedType::CONNECTED) { - status_icon = checkmark; - } else if (network.security_type == SecurityType::UNSUPPORTED) { - status_icon = circled_slash; - } else if (network.security_type == SecurityType::WPA) { - status_icon = lock; - } - bool show_forget_btn = wifi->isKnownConnection(network.ssid) && !is_tethering_enabled; - QPixmap strength = strengths[strengthLevel(network.strength)]; - - auto item = getItem(n++); - item->setItem(network, status_icon, show_forget_btn, strength); - item->setVisible(true); - } - for (; n < wifi_items.size(); ++n) wifi_items[n]->setVisible(false); - - setUpdatesEnabled(true); -} - -WifiItem *WifiUI::getItem(int n) { - auto item = n < wifi_items.size() ? wifi_items[n] : wifi_items.emplace_back(new WifiItem(tr("CONNECTING..."), tr("FORGET"))); - if (!item->parentWidget()) { - QObject::connect(item, &WifiItem::connectToNetwork, this, &WifiUI::connectToNetwork); - QObject::connect(item, &WifiItem::forgotNetwork, [this](const Network n) { - if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"%1\"?").arg(QString::fromUtf8(n.ssid)), tr("Forget"), this)) - wifi->forgetConnection(n.ssid); - }); - wifi_list_widget->addItem(item); - } - return item; -} - -// WifiItem - -WifiItem::WifiItem(const QString &connecting_text, const QString &forget_text, QWidget *parent) : QWidget(parent) { - QHBoxLayout *hlayout = new QHBoxLayout(this); - hlayout->setContentsMargins(44, 0, 73, 0); - hlayout->setSpacing(50); - - hlayout->addWidget(ssidLabel = new ElidedLabel()); - ssidLabel->setObjectName("ssidLabel"); - ssidLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - hlayout->addWidget(connecting = new QPushButton(connecting_text), 0, Qt::AlignRight); - connecting->setObjectName("connecting"); - hlayout->addWidget(forgetBtn = new QPushButton(forget_text), 0, Qt::AlignRight); - forgetBtn->setObjectName("forgetBtn"); - hlayout->addWidget(iconLabel = new QLabel(), 0, Qt::AlignRight); - hlayout->addWidget(strengthLabel = new QLabel(), 0, Qt::AlignRight); - - iconLabel->setFixedWidth(ICON_WIDTH); - QObject::connect(forgetBtn, &QPushButton::clicked, [this]() { emit forgotNetwork(network); }); - QObject::connect(ssidLabel, &ElidedLabel::clicked, [this]() { - if (network.connected == ConnectedType::DISCONNECTED) emit connectToNetwork(network); - }); -} - -void WifiItem::setItem(const Network &n, const QPixmap &status_icon, bool show_forget_btn, const QPixmap &strength_icon) { - network = n; - - ssidLabel->setText(n.ssid); - ssidLabel->setEnabled(n.security_type != SecurityType::UNSUPPORTED); - ssidLabel->setFont(InterFont(55, network.connected == ConnectedType::DISCONNECTED ? QFont::Normal : QFont::Bold)); - - connecting->setVisible(n.connected == ConnectedType::CONNECTING); - forgetBtn->setVisible(show_forget_btn); - - iconLabel->setPixmap(status_icon); - strengthLabel->setPixmap(strength_icon); -} diff --git a/selfdrive/ui/qt/network/networking.h b/selfdrive/ui/qt/network/networking.h deleted file mode 100644 index 73c2226a34..0000000000 --- a/selfdrive/ui/qt/network/networking.h +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include - -#include "selfdrive/ui/qt/network/wifi_manager.h" -#include "selfdrive/ui/qt/prime_state.h" -#include "selfdrive/ui/qt/widgets/input.h" -#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 MultiButtonControl MultiButtonControlSP -#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: - explicit WifiItem(const QString &connecting_text, const QString &forget_text, QWidget* parent = nullptr); - void setItem(const Network& n, const QPixmap &icon, bool show_forget_btn, const QPixmap &strength); - -signals: - // Cannot pass Network by reference. it may change after the signal is sent. - void connectToNetwork(const Network n); - void forgotNetwork(const Network n); - -protected: - ElidedLabel* ssidLabel; - QPushButton* connecting; - QPushButton* forgetBtn; - QLabel* iconLabel; - QLabel* strengthLabel; - Network network; -}; - -class WifiUI : public QWidget { - Q_OBJECT - -public: - explicit WifiUI(QWidget *parent = 0, WifiManager* wifi = 0); - -private: - WifiItem *getItem(int n); - - WifiManager *wifi = nullptr; - QLabel *scanningLabel = nullptr; - QPixmap lock; - QPixmap checkmark; - QPixmap circled_slash; - QVector strengths; - ListWidget *wifi_list_widget = nullptr; - std::vector wifi_items; - -signals: - void connectToNetwork(const Network n); - -public slots: - void refresh(); -}; - -class AdvancedNetworking : public QWidget { - Q_OBJECT -public: - explicit AdvancedNetworking(QWidget* parent = 0, WifiManager* wifi = 0); - void setGsmVisible(bool visible); - -private: - LabelControl* ipLabel; - ToggleControl* tetheringToggle; - ToggleControl* roamingToggle; - ButtonControl* editApnButton; - ButtonControl* hiddenNetworkButton; - ToggleControl* cellularMeteredToggle; - MultiButtonControl* wifiMeteredToggle; - WifiManager* wifi = nullptr; - Params params; - -signals: - void backPress(); - void requestWifiScreen(); - -public slots: - void toggleTethering(bool enabled); - void refresh(); -}; - -class Networking : public QFrame { - Q_OBJECT - -public: - explicit Networking(QWidget* parent = 0, bool show_advanced = true); - void setPrimeType(PrimeState::Type type); - WifiManager* wifi = nullptr; - -protected: - QStackedLayout* main_layout = nullptr; - QWidget* wifiScreen = nullptr; - AdvancedNetworking* an = nullptr; - WifiUI* wifiWidget; - - void showEvent(QShowEvent* event) override; - void hideEvent(QHideEvent* event) override; - -public slots: - void refresh(); - -private slots: - void connectToNetwork(const Network n); - void wrongPassword(const QString &ssid); -}; diff --git a/selfdrive/ui/qt/network/networkmanager.h b/selfdrive/ui/qt/network/networkmanager.h deleted file mode 100644 index 8bdeaf3bbd..0000000000 --- a/selfdrive/ui/qt/network/networkmanager.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -/** - * We are using a NetworkManager DBUS API : https://developer.gnome.org/NetworkManager/1.26/spec.html - * */ - -// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApFlags -const int NM_802_11_AP_FLAGS_NONE = 0x00000000; -const int NM_802_11_AP_FLAGS_PRIVACY = 0x00000001; -const int NM_802_11_AP_FLAGS_WPS = 0x00000002; - -// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApSecurityFlags -const int NM_802_11_AP_SEC_PAIR_WEP40 = 0x00000001; -const int NM_802_11_AP_SEC_PAIR_WEP104 = 0x00000002; -const int NM_802_11_AP_SEC_GROUP_WEP40 = 0x00000010; -const int NM_802_11_AP_SEC_GROUP_WEP104 = 0x00000020; -const int NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x00000100; -const int NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200; - -const QString NM_DBUS_PATH = "/org/freedesktop/NetworkManager"; -const QString NM_DBUS_PATH_SETTINGS = "/org/freedesktop/NetworkManager/Settings"; - -const QString NM_DBUS_INTERFACE = "org.freedesktop.NetworkManager"; -const QString NM_DBUS_INTERFACE_PROPERTIES = "org.freedesktop.DBus.Properties"; -const QString NM_DBUS_INTERFACE_SETTINGS = "org.freedesktop.NetworkManager.Settings"; -const QString NM_DBUS_INTERFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"; -const QString NM_DBUS_INTERFACE_DEVICE = "org.freedesktop.NetworkManager.Device"; -const QString NM_DBUS_INTERFACE_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless"; -const QString NM_DBUS_INTERFACE_ACCESS_POINT = "org.freedesktop.NetworkManager.AccessPoint"; -const QString NM_DBUS_INTERFACE_ACTIVE_CONNECTION = "org.freedesktop.NetworkManager.Connection.Active"; -const QString NM_DBUS_INTERFACE_IP4_CONFIG = "org.freedesktop.NetworkManager.IP4Config"; - -const QString NM_DBUS_SERVICE = "org.freedesktop.NetworkManager"; - -const int NM_DEVICE_STATE_UNKNOWN = 0; -const int NM_DEVICE_STATE_ACTIVATED = 100; -const int NM_DEVICE_STATE_NEED_AUTH = 60; -const int NM_DEVICE_TYPE_WIFI = 2; -const int NM_DEVICE_TYPE_MODEM = 8; -const int NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8; -const int DBUS_TIMEOUT = 100; - -// https://developer-old.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMMetered -const int NM_METERED_UNKNOWN = 0; -const int NM_METERED_YES = 1; -const int NM_METERED_NO = 2; -const int NM_METERED_GUESS_YES = 3; -const int NM_METERED_GUESS_NO = 4; diff --git a/selfdrive/ui/qt/network/wifi_manager.cc b/selfdrive/ui/qt/network/wifi_manager.cc deleted file mode 100644 index 8a104c0fbe..0000000000 --- a/selfdrive/ui/qt/network/wifi_manager.cc +++ /dev/null @@ -1,539 +0,0 @@ -#include "selfdrive/ui/qt/network/wifi_manager.h" - -#include - -#include "common/swaglog.h" -#include "selfdrive/ui/qt/util.h" - -bool compare_by_strength(const Network &a, const Network &b) { - return std::tuple(a.connected, strengthLevel(a.strength), b.ssid) > - std::tuple(b.connected, strengthLevel(b.strength), a.ssid); -} - -template -T call(const QString &path, const QString &interface, const QString &method, Args &&...args) { - QDBusInterface nm(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus()); - nm.setTimeout(DBUS_TIMEOUT); - - QDBusMessage response = nm.call(method, std::forward(args)...); - if (response.type() == QDBusMessage::ErrorMessage) { - qCritical() << "DBus call error:" << response.errorMessage(); - return T(); - } - - if constexpr (std::is_same_v) { - return response; - } else if (response.arguments().count() >= 1) { - QVariant vFirst = response.arguments().at(0).value().variant(); - if (vFirst.canConvert()) { - return vFirst.value(); - } - QDebug critical = qCritical(); - critical << "Variant unpacking failure :" << method << ','; - (critical << ... << args); - } - return T(); -} - -template -QDBusPendingCall asyncCall(const QString &path, const QString &interface, const QString &method, Args &&...args) { - QDBusInterface nm = QDBusInterface(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus()); - return nm.asyncCall(method, args...); -} - -bool emptyPath(const QString &path) { - return path == "" || path == "/"; -} - -WifiManager::WifiManager(QObject *parent) : QObject(parent) { - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - - // Set tethering ssid as "weedle" + first 4 characters of a dongle id - tethering_ssid = "weedle"; - if (auto dongle_id = getDongleId()) { - tethering_ssid += "-" + dongle_id->left(4); - } - - adapter = getAdapter(); - if (!adapter.isEmpty()) { - setup(); - } else { - QDBusConnection::systemBus().connect(NM_DBUS_SERVICE, NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeviceAdded", this, SLOT(deviceAdded(QDBusObjectPath))); - } - - timer.callOnTimeout(this, &WifiManager::requestScan); - - initConnections(); -} - -void WifiManager::setup() { - auto bus = QDBusConnection::systemBus(); - bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_DEVICE, "StateChanged", this, SLOT(stateChange(unsigned int, unsigned int, unsigned int))); - bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_PROPERTIES, "PropertiesChanged", this, SLOT(propertyChange(QString, QVariantMap, QStringList))); - - bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ConnectionRemoved", this, SLOT(connectionRemoved(QDBusObjectPath))); - bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "NewConnection", this, SLOT(newConnection(QDBusObjectPath))); - - raw_adapter_state = call(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "State"); - activeAp = call(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE_WIRELESS, "ActiveAccessPoint").path(); - - requestScan(); -} - -void WifiManager::start() { - timer.start(5000); - refreshNetworks(); -} - -void WifiManager::stop() { - timer.stop(); -} - -void WifiManager::refreshNetworks() { - if (adapter.isEmpty() || !timer.isActive()) return; - - QDBusPendingCall pending_call = asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "GetAllAccessPoints"); - QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending_call); - QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::refreshFinished); -} - -void WifiManager::refreshFinished(QDBusPendingCallWatcher *watcher) { - ipv4_address = getIp4Address(); - seenNetworks.clear(); - - const QDBusReply> watcher_reply = *watcher; - if (!watcher_reply.isValid()) { - qCritical() << "Failed to refresh"; - watcher->deleteLater(); - return; - } - - for (const QDBusObjectPath &path : watcher_reply.value()) { - QDBusReply reply = call(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "GetAll", NM_DBUS_INTERFACE_ACCESS_POINT); - if (!reply.isValid()) { - qCritical() << "Failed to retrieve properties for path:" << path.path(); - continue; - } - - auto properties = reply.value(); - const QByteArray ssid = properties["Ssid"].toByteArray(); - if (ssid.isEmpty()) continue; - - // May be multiple access points for each SSID. - // Use first for ssid and security type, then update connected status and strength using all - if (!seenNetworks.contains(ssid)) { - seenNetworks[ssid] = {ssid, 0U, ConnectedType::DISCONNECTED, getSecurityType(properties)}; - } - - if (path.path() == activeAp) { - seenNetworks[ssid].connected = (ssid == connecting_to_network) ? ConnectedType::CONNECTING : ConnectedType::CONNECTED; - } - - uint32_t strength = properties["Strength"].toUInt(); - if (seenNetworks[ssid].strength < strength) { - seenNetworks[ssid].strength = strength; - } - } - - emit refreshSignal(); - watcher->deleteLater(); -} - -QString WifiManager::getIp4Address() { - if (raw_adapter_state != NM_DEVICE_STATE_ACTIVATED) return ""; - - for (const auto &p : getActiveConnections()) { - QString type = call(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); - if (type == "802-11-wireless") { - auto ip4config = call(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Ip4Config"); - const auto &arr = call(ip4config.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_IP4_CONFIG, "AddressData"); - QVariantMap path; - arr.beginArray(); - while (!arr.atEnd()) { - arr >> path; - arr.endArray(); - return path.value("address").value(); - } - arr.endArray(); - } - } - return ""; -} - -SecurityType WifiManager::getSecurityType(const QVariantMap &properties) { - int sflag = properties["Flags"].toUInt(); - int wpaflag = properties["WpaFlags"].toUInt(); - int rsnflag = properties["RsnFlags"].toUInt(); - int wpa_props = wpaflag | rsnflag; - - // obtained by looking at flags of networks in the office as reported by an Android phone - const int supports_wpa = NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 | NM_802_11_AP_SEC_GROUP_WEP40 | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_KEY_MGMT_PSK; - - if ((sflag == NM_802_11_AP_FLAGS_NONE) || ((sflag & NM_802_11_AP_FLAGS_WPS) && !(wpa_props & supports_wpa))) { - return SecurityType::OPEN; - } else if ((sflag & NM_802_11_AP_FLAGS_PRIVACY) && (wpa_props & supports_wpa) && !(wpa_props & NM_802_11_AP_SEC_KEY_MGMT_802_1X)) { - return SecurityType::WPA; - } else { - LOGW("Unsupported network! sflag: %d, wpaflag: %d, rsnflag: %d", sflag, wpaflag, rsnflag); - return SecurityType::UNSUPPORTED; - } -} - -void WifiManager::connect(const Network &n, const bool is_hidden, const QString &password, const QString &username) { - setCurrentConnecting(n.ssid); - forgetConnection(n.ssid); // Clear all connections that may already exist to the network we are connecting - Connection connection; - connection["connection"]["type"] = "802-11-wireless"; - connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); - connection["connection"]["id"] = "sunnypilot connection " + QString::fromStdString(n.ssid.toStdString()); - connection["connection"]["autoconnect-retries"] = 0; - - connection["802-11-wireless"]["ssid"] = n.ssid; - connection["802-11-wireless"]["hidden"] = is_hidden; - connection["802-11-wireless"]["mode"] = "infrastructure"; - - if (n.security_type == SecurityType::WPA) { - connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"; - connection["802-11-wireless-security"]["auth-alg"] = "open"; - connection["802-11-wireless-security"]["psk"] = password; - } - - connection["ipv4"]["method"] = "auto"; - connection["ipv4"]["dns-priority"] = 600; - connection["ipv6"]["method"] = "ignore"; - - asyncCall(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection)); -} - -void WifiManager::deactivateConnectionBySsid(const QString &ssid) { - for (QDBusObjectPath active_connection : getActiveConnections()) { - auto pth = call(active_connection.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "SpecificObject"); - if (!emptyPath(pth.path())) { - QString Ssid = get_property(pth.path(), "Ssid"); - if (Ssid == ssid) { - deactivateConnection(active_connection); - return; - } - } - } -} - -void WifiManager::deactivateConnection(const QDBusObjectPath &path) { - asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeactivateConnection", QVariant::fromValue(path)); -} - -QVector WifiManager::getActiveConnections() { - auto result = call(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "ActiveConnections"); - return qdbus_cast>(result); -} - -bool WifiManager::isKnownConnection(const QString &ssid) { - return !getConnectionPath(ssid).path().isEmpty(); -} - -void WifiManager::forgetConnection(const QString &ssid) { - const QDBusObjectPath &path = getConnectionPath(ssid); - if (!path.path().isEmpty()) { - call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Delete"); - } -} - -void WifiManager::setCurrentConnecting(const QString &ssid) { - connecting_to_network = ssid; - for (auto &network : seenNetworks) { - network.connected = (network.ssid == ssid) ? ConnectedType::CONNECTING : ConnectedType::DISCONNECTED; - } - emit refreshSignal(); -} - -uint WifiManager::getAdapterType(const QDBusObjectPath &path) { - return call(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "DeviceType"); -} - -void WifiManager::requestScan() { - if (!adapter.isEmpty()) { - asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "RequestScan", QVariantMap()); - } -} - -QByteArray WifiManager::get_property(const QString &network_path , const QString &property) { - return call(network_path, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACCESS_POINT, property); -} - -QString WifiManager::getAdapter(const uint adapter_type) { - QDBusReply> response = call(NM_DBUS_PATH, NM_DBUS_INTERFACE, "GetDevices"); - for (const QDBusObjectPath &path : response.value()) { - if (getAdapterType(path) == adapter_type) { - return path.path(); - } - } - return ""; -} - -void WifiManager::stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason) { - raw_adapter_state = new_state; - if (new_state == NM_DEVICE_STATE_NEED_AUTH && change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT && !connecting_to_network.isEmpty()) { - forgetConnection(connecting_to_network); - emit wrongPassword(connecting_to_network); - } else if (new_state == NM_DEVICE_STATE_ACTIVATED) { - connecting_to_network = ""; - refreshNetworks(); - } -} - -// https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html -void WifiManager::propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props) { - if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("LastScan")) { - refreshNetworks(); - } else if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("ActiveAccessPoint")) { - activeAp = props.value("ActiveAccessPoint").value().path(); - } -} - -void WifiManager::deviceAdded(const QDBusObjectPath &path) { - if (getAdapterType(path) == NM_DEVICE_TYPE_WIFI && emptyPath(adapter)) { - adapter = path.path(); - setup(); - } -} - -void WifiManager::connectionRemoved(const QDBusObjectPath &path) { - knownConnections.remove(path); -} - -void WifiManager::newConnection(const QDBusObjectPath &path) { - Connection settings = getConnectionSettings(path); - if (settings.value("connection").value("type") == "802-11-wireless") { - knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString(); - if (knownConnections[path] != tethering_ssid) { - activateWifiConnection(knownConnections[path]); - } - } -} - -QDBusObjectPath WifiManager::getConnectionPath(const QString &ssid) { - return knownConnections.key(ssid); -} - -Connection WifiManager::getConnectionSettings(const QDBusObjectPath &path) { - return QDBusReply(call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSettings")).value(); -} - -void WifiManager::initConnections() { - const QDBusReply> response = call(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ListConnections"); - for (const QDBusObjectPath &path : response.value()) { - const Connection settings = getConnectionSettings(path); - if (settings.value("connection").value("type") == "802-11-wireless") { - knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString(); - } else if (settings.value("connection").value("id") == "lte") { - lteConnectionPath = path; - } - } - - if (!isKnownConnection(tethering_ssid)) { - addTetheringConnection(); - } -} - -std::optional WifiManager::activateWifiConnection(const QString &ssid) { - const QDBusObjectPath &path = getConnectionPath(ssid); - if (!path.path().isEmpty()) { - setCurrentConnecting(ssid); - return asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(adapter)), QVariant::fromValue(QDBusObjectPath("/"))); - } - return std::nullopt; -} - -void WifiManager::activateModemConnection(const QDBusObjectPath &path) { - QString modem = getAdapter(NM_DEVICE_TYPE_MODEM); - if (!path.path().isEmpty() && !modem.isEmpty()) { - asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(modem)), QVariant::fromValue(QDBusObjectPath("/"))); - } -} - -// function matches tici/hardware.py -// FIXME: it can mistakenly show CELL when connected to WIFI -NetworkType WifiManager::currentNetworkType() { - auto primary_conn = call(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "PrimaryConnection"); - auto primary_type = call(primary_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); - - if (primary_type == "802-3-ethernet") { - return NetworkType::ETHERNET; - } else if (primary_type == "802-11-wireless" && !isTetheringEnabled()) { - return NetworkType::WIFI; - } else { - for (const QDBusObjectPath &conn : getActiveConnections()) { - auto type = call(conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); - if (type == "gsm") { - return NetworkType::CELL; - } - } - } - return NetworkType::NONE; -} - -MeteredType WifiManager::currentNetworkMetered() { - MeteredType metered = MeteredType::UNKNOWN; - for (const auto &active_conn : getActiveConnections()) { - QString type = call(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); - if (type == "802-11-wireless") { - QDBusObjectPath conn = call(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Connection"); - if (!conn.path().isEmpty()) { - Connection settings = getConnectionSettings(conn); - int metered_prop = settings.value("connection").value("metered").toInt(); - if (metered_prop == NM_METERED_YES) { - metered = MeteredType::YES; - } else if (metered_prop == NM_METERED_NO) { - metered = MeteredType::NO; - } - } - break; - } - } - return metered; -} - -std::optional WifiManager::setCurrentNetworkMetered(MeteredType metered) { - for (const auto &active_conn : getActiveConnections()) { - QString type = call(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); - if (type == "802-11-wireless") { - if (!isTetheringEnabled()) { - QDBusObjectPath conn = call(active_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Connection"); - if (!conn.path().isEmpty()) { - Connection settings = getConnectionSettings(conn); - settings["connection"]["metered"] = static_cast(metered); - return asyncCall(conn.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Update", QVariant::fromValue(settings)); - } - } - } - } - return std::nullopt; -} - -void WifiManager::updateGsmSettings(bool roaming, QString apn, bool metered) { - if (!lteConnectionPath.path().isEmpty()) { - bool changes = false; - bool auto_config = apn.isEmpty(); - Connection settings = getConnectionSettings(lteConnectionPath); - if (settings.value("gsm").value("auto-config").toBool() != auto_config) { - qWarning() << "Changing gsm.auto-config to" << auto_config; - settings["gsm"]["auto-config"] = auto_config; - changes = true; - } - - if (settings.value("gsm").value("apn").toString() != apn) { - qWarning() << "Changing gsm.apn to" << apn; - settings["gsm"]["apn"] = apn; - changes = true; - } - - if (settings.value("gsm").value("home-only").toBool() == roaming) { - qWarning() << "Changing gsm.home-only to" << !roaming; - settings["gsm"]["home-only"] = !roaming; - changes = true; - } - - int meteredInt = metered ? NM_METERED_UNKNOWN : NM_METERED_NO; - if (settings.value("connection").value("metered").toInt() != meteredInt) { - qWarning() << "Changing connection.metered to" << meteredInt; - settings["connection"]["metered"] = meteredInt; - changes = true; - } - - if (changes) { - QDBusPendingCall pending_call = asyncCall(lteConnectionPath.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "UpdateUnsaved", QVariant::fromValue(settings)); // update is temporary - QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending_call); - QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher]() { - deactivateConnection(lteConnectionPath); - activateModemConnection(lteConnectionPath); - watcher->deleteLater(); - }); - } - } -} - -// Functions for tethering -void WifiManager::addTetheringConnection() { - Connection connection; - connection["connection"]["id"] = "Hotspot"; - connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); - connection["connection"]["type"] = "802-11-wireless"; - connection["connection"]["interface-name"] = "wlan0"; - connection["connection"]["autoconnect"] = false; - - connection["802-11-wireless"]["band"] = "bg"; - connection["802-11-wireless"]["mode"] = "ap"; - connection["802-11-wireless"]["ssid"] = tethering_ssid.toUtf8(); - - connection["802-11-wireless-security"]["group"] = QStringList("ccmp"); - connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"; - connection["802-11-wireless-security"]["pairwise"] = QStringList("ccmp"); - connection["802-11-wireless-security"]["proto"] = QStringList("rsn"); - connection["802-11-wireless-security"]["psk"] = defaultTetheringPassword; - - connection["ipv4"]["method"] = "shared"; - QVariantMap address; - address["address"] = "192.168.43.1"; - address["prefix"] = 24u; - connection["ipv4"]["address-data"] = QVariant::fromValue(IpConfig() << address); - connection["ipv4"]["gateway"] = "192.168.43.1"; - connection["ipv4"]["never-default"] = true; - connection["ipv6"]["method"] = "ignore"; - - asyncCall(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection)); -} - -void WifiManager::tetheringActivated(QDBusPendingCallWatcher *call) { - if (!ipv4_forward) { - QTimer::singleShot(5000, this, [=] { - qWarning() << "net.ipv4.ip_forward = 0"; - std::system("sudo sysctl net.ipv4.ip_forward=0"); - }); - } - call->deleteLater(); - tethering_on = true; -} - -void WifiManager::setTetheringEnabled(bool enabled) { - if (enabled) { - auto pending_call = activateWifiConnection(tethering_ssid); - - if (pending_call) { - QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(*pending_call); - QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::tetheringActivated); - } - - } else { - deactivateConnectionBySsid(tethering_ssid); - tethering_on = false; - } -} - -bool WifiManager::isTetheringEnabled() { - if (!emptyPath(activeAp)) { - return get_property(activeAp, "Ssid") == tethering_ssid; - } - return false; -} - -QString WifiManager::getTetheringPassword() { - const QDBusObjectPath &path = getConnectionPath(tethering_ssid); - if (!path.path().isEmpty()) { - QDBusReply> response = call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSecrets", "802-11-wireless-security"); - return response.value().value("802-11-wireless-security").value("psk").toString(); - } - return ""; -} - -void WifiManager::changeTetheringPassword(const QString &newPassword) { - const QDBusObjectPath &path = getConnectionPath(tethering_ssid); - if (!path.path().isEmpty()) { - Connection settings = getConnectionSettings(path); - settings["802-11-wireless-security"]["psk"] = newPassword; - call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Update", QVariant::fromValue(settings)); - if (isTetheringEnabled()) { - activateWifiConnection(tethering_ssid); - } - } -} diff --git a/selfdrive/ui/qt/network/wifi_manager.h b/selfdrive/ui/qt/network/wifi_manager.h deleted file mode 100644 index cab932a388..0000000000 --- a/selfdrive/ui/qt/network/wifi_manager.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "selfdrive/ui/qt/network/networkmanager.h" - -enum class SecurityType { - OPEN, - WPA, - UNSUPPORTED -}; -enum class ConnectedType { - DISCONNECTED, - CONNECTING, - CONNECTED -}; -enum class NetworkType { - NONE, - WIFI, - CELL, - ETHERNET -}; -enum class MeteredType { - UNKNOWN, - YES, - NO -}; - -typedef QMap Connection; -typedef QVector IpConfig; - -struct Network { - QByteArray ssid; - unsigned int strength; - ConnectedType connected; - SecurityType security_type; -}; -bool compare_by_strength(const Network &a, const Network &b); -inline int strengthLevel(unsigned int strength) { return std::clamp((int)round(strength / 33.), 0, 3); } - -class WifiManager : public QObject { - Q_OBJECT - -public: - QMap seenNetworks; - QMap knownConnections; - QString ipv4_address; - bool tethering_on = false; - bool ipv4_forward = false; - - explicit WifiManager(QObject* parent); - void start(); - void stop(); - void requestScan(); - void forgetConnection(const QString &ssid); - bool isKnownConnection(const QString &ssid); - std::optional activateWifiConnection(const QString &ssid); - NetworkType currentNetworkType(); - MeteredType currentNetworkMetered(); - std::optional setCurrentNetworkMetered(MeteredType metered); - void updateGsmSettings(bool roaming, QString apn, bool metered); - void connect(const Network &ssid, const bool is_hidden = false, const QString &password = {}, const QString &username = {}); - - // Tethering functions - void setTetheringEnabled(bool enabled); - bool isTetheringEnabled(); - void changeTetheringPassword(const QString &newPassword); - QString getTetheringPassword(); - -private: - QString adapter; // Path to network manager wifi-device - QTimer timer; - unsigned int raw_adapter_state = NM_DEVICE_STATE_UNKNOWN; // Connection status https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMDeviceState - QString connecting_to_network; - QString tethering_ssid; - const QString defaultTetheringPassword = "swagswagcomma"; - QString activeAp; - QDBusObjectPath lteConnectionPath; - - QString getAdapter(const uint = NM_DEVICE_TYPE_WIFI); - uint getAdapterType(const QDBusObjectPath &path); - QString getIp4Address(); - void deactivateConnectionBySsid(const QString &ssid); - void deactivateConnection(const QDBusObjectPath &path); - QVector getActiveConnections(); - QByteArray get_property(const QString &network_path, const QString &property); - SecurityType getSecurityType(const QVariantMap &properties); - QDBusObjectPath getConnectionPath(const QString &ssid); - Connection getConnectionSettings(const QDBusObjectPath &path); - void initConnections(); - void setup(); - void refreshNetworks(); - void activateModemConnection(const QDBusObjectPath &path); - void addTetheringConnection(); - void setCurrentConnecting(const QString &ssid); - -signals: - void wrongPassword(const QString &ssid); - void refreshSignal(); - -private slots: - void stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason); - void propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props); - void deviceAdded(const QDBusObjectPath &path); - void connectionRemoved(const QDBusObjectPath &path); - void newConnection(const QDBusObjectPath &path); - void refreshFinished(QDBusPendingCallWatcher *call); - void tetheringActivated(QDBusPendingCallWatcher *call); -}; diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc deleted file mode 100644 index 37c5d19272..0000000000 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ /dev/null @@ -1,103 +0,0 @@ -#include "selfdrive/ui/qt/offroad/developer_panel.h" -#include "selfdrive/ui/qt/widgets/ssh_keys.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) { - adbToggle = new ParamControl("AdbEnabled", tr("Enable ADB"), - tr("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."), ""); - addItem(adbToggle); - - // SSH keys - addItem(new SshToggle()); - addItem(new SshControl()); - - joystickToggle = new ParamControl("JoystickDebugMode", tr("Joystick Debug Mode"), "", ""); - QObject::connect(joystickToggle, &ParamControl::toggleFlipped, [=](bool state) { - params.putBool("LongitudinalManeuverMode", false); - longManeuverToggle->refresh(); - }); - addItem(joystickToggle); - - longManeuverToggle = new ParamControl("LongitudinalManeuverMode", tr("Longitudinal Maneuver Mode"), "", ""); - QObject::connect(longManeuverToggle, &ParamControl::toggleFlipped, [=](bool state) { - params.putBool("JoystickDebugMode", false); - joystickToggle->refresh(); - }); - addItem(longManeuverToggle); - - experimentalLongitudinalToggle = new ParamControl( - "AlphaLongitudinalEnabled", - tr("sunnypilot Longitudinal Control (Alpha)"), - QString("%1

%2") - .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); - QObject::connect(experimentalLongitudinalToggle, &ParamControl::toggleFlipped, [=]() { - updateToggles(offroad); - }); - addItem(experimentalLongitudinalToggle); - - // Joystick and longitudinal maneuvers should be hidden on release branches - is_release = params.getBool("IsReleaseBranch"); - - // Toggles should be not available to change in onroad state - QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanel::updateToggles); -} - -void DeveloperPanel::updateToggles(bool _offroad) { - for (auto btn : findChildren()) { - btn->setVisible(!is_release); - - /* - * experimentalLongitudinalToggle should be toggelable when: - * - visible, and - * - during onroad & offroad states - */ - if (btn != experimentalLongitudinalToggle) { - btn->setEnabled(_offroad); - } - } - - // longManeuverToggle and experimentalLongitudinalToggle should not be toggleable if the car does not have longitudinal control - 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.getAlphaLongitudinalAvailable() || is_release) { - params.remove("AlphaLongitudinalEnabled"); - experimentalLongitudinalToggle->setEnabled(false); - } - - /* - * experimentalLongitudinalToggle should be visible when: - * - is not a release branch, and - * - the car supports experimental longitudinal control (alpha) - */ - experimentalLongitudinalToggle->setVisible(CP.getAlphaLongitudinalAvailable() && !is_release); - - longManeuverToggle->setEnabled(hasLongitudinalControl(CP) && _offroad); - } else { - longManeuverToggle->setEnabled(false); - experimentalLongitudinalToggle->setVisible(false); - } - experimentalLongitudinalToggle->refresh(); - - // Handle specific controls visibility for release branches - joystickToggle->setVisible(!is_release); - - offroad = _offroad; -} - -void DeveloperPanel::showEvent(QShowEvent *event) { - updateToggles(offroad); -} diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h deleted file mode 100644 index 699fa1edcf..0000000000 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ /dev/null @@ -1,26 +0,0 @@ -#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 -public: - explicit DeveloperPanel(SettingsWindow *parent); - void showEvent(QShowEvent *event) override; - -protected: - Params params; - ParamControl* adbToggle; - ParamControl* joystickToggle; - ParamControl* longManeuverToggle; - ParamControl* experimentalLongitudinalToggle; - bool is_release; - bool offroad = false; - -private slots: - void updateToggles(bool _offroad); -}; diff --git a/selfdrive/ui/qt/offroad/driverview.cc b/selfdrive/ui/qt/offroad/driverview.cc deleted file mode 100644 index 9010227f18..0000000000 --- a/selfdrive/ui/qt/offroad/driverview.cc +++ /dev/null @@ -1,82 +0,0 @@ -#include "selfdrive/ui/qt/offroad/driverview.h" - -#include -#include - -#include "selfdrive/ui/qt/util.h" - -DriverViewWindow::DriverViewWindow(QWidget* parent) : CameraWidget("camerad", VISION_STREAM_DRIVER, parent) { - QObject::connect(this, &CameraWidget::clicked, this, &DriverViewWindow::done); - QObject::connect(device(), &Device::interactiveTimeout, this, [this]() { - if (isVisible()) { - emit done(); - } - }); -} - -void DriverViewWindow::showEvent(QShowEvent* event) { - params.putBool("IsDriverViewEnabled", true); - device()->resetInteractiveTimeout(60); - CameraWidget::showEvent(event); -} - -void DriverViewWindow::hideEvent(QHideEvent* event) { - params.putBool("IsDriverViewEnabled", false); - stopVipcThread(); - CameraWidget::hideEvent(event); -} - -void DriverViewWindow::paintGL() { - CameraWidget::paintGL(); - - std::lock_guard lk(frame_lock); - QPainter p(this); - // startup msg - if (frames.empty()) { - p.setPen(Qt::white); - p.setRenderHint(QPainter::TextAntialiasing); - p.setFont(InterFont(100, QFont::Bold)); - p.drawText(geometry(), Qt::AlignCenter, tr("camera starting")); - return; - } - - const auto &sm = *(uiState()->sm); - cereal::DriverStateV2::Reader driver_state = sm["driverStateV2"].getDriverStateV2(); - bool is_rhd = driver_state.getWheelOnRightProb() > 0.5; - auto driver_data = is_rhd ? driver_state.getRightDriverData() : driver_state.getLeftDriverData(); - - bool face_detected = driver_data.getFaceProb() > 0.7; - if (face_detected) { - auto fxy_list = driver_data.getFacePosition(); - auto std_list = driver_data.getFaceOrientationStd(); - float face_x = fxy_list[0]; - float face_y = fxy_list[1]; - float face_std = std::max(std_list[0], std_list[1]); - - float alpha = 0.7; - if (face_std > 0.15) { - alpha = std::max(0.7 - (face_std-0.15)*3.5, 0.0); - } - const int box_size = 220; - // use approx instead of distort_points - int fbox_x = 1080.0 - 1714.0 * face_x; - int fbox_y = -135.0 + (504.0 + std::abs(face_x)*112.0) + (1205.0 - std::abs(face_x)*724.0) * face_y; - p.setPen(QPen(QColor(255, 255, 255, alpha * 255), 10)); - p.drawRoundedRect(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size, 35.0, 35.0); - } - - driver_monitor.updateState(*uiState()); - driver_monitor.draw(p, rect()); -} - -mat4 DriverViewWindow::calcFrameMatrix() { - const float driver_view_ratio = 2.0; - const float yscale = stream_height * driver_view_ratio / stream_width; - const float xscale = yscale * glHeight() / glWidth() * stream_width / stream_height; - return mat4{{ - xscale, 0.0, 0.0, 0.0, - 0.0, yscale, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - }}; -} diff --git a/selfdrive/ui/qt/offroad/driverview.h b/selfdrive/ui/qt/offroad/driverview.h deleted file mode 100644 index f6eb752fe6..0000000000 --- a/selfdrive/ui/qt/offroad/driverview.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "selfdrive/ui/qt/widgets/cameraview.h" -#include "selfdrive/ui/qt/onroad/driver_monitoring.h" - -class DriverViewWindow : public CameraWidget { - Q_OBJECT - -public: - explicit DriverViewWindow(QWidget *parent); - -signals: - void done(); - -protected: - mat4 calcFrameMatrix() override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - void paintGL() override; - - Params params; - DriverMonitorRenderer driver_monitor; -}; diff --git a/selfdrive/ui/qt/offroad/experimental_mode.cc b/selfdrive/ui/qt/offroad/experimental_mode.cc deleted file mode 100644 index a3288a3e96..0000000000 --- a/selfdrive/ui/qt/offroad/experimental_mode.cc +++ /dev/null @@ -1,82 +0,0 @@ -#include "selfdrive/ui/qt/offroad/experimental_mode.h" - -#include -#include -#include -#include -#include - -#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/icons/couch.svg").scaledToWidth(img_width, Qt::SmoothTransformation); - experimental_pixmap = QPixmap("../assets/icons/experimental_grey.svg").scaledToWidth(img_width, Qt::SmoothTransformation); - - // go to toggles and expand experimental mode description - connect(this, &QPushButton::clicked, [=]() { emit openSettings(toggles_settings_index, "ExperimentalMode"); }); - - setFixedHeight(125); - QHBoxLayout *main_layout = new QHBoxLayout; - main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0); - - mode_label = new QLabel; - mode_icon = new QLabel; - mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); - - main_layout->addWidget(mode_label, 1, Qt::AlignLeft); - main_layout->addWidget(mode_icon, 0, Qt::AlignRight); - - setLayout(main_layout); - - setStyleSheet(R"( - QPushButton { - border: none; - } - - QLabel { - font-size: 45px; - font-weight: 300; - text-align: left; - font-family: JetBrainsMono; - color: #000000; - } - )"); -} - -void ExperimentalModeButton::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.setPen(Qt::NoPen); - p.setRenderHint(QPainter::Antialiasing); - - QPainterPath path; - path.addRoundedRect(rect(), 10, 10); - - // gradient - bool pressed = isDown(); - QLinearGradient gradient(rect().left(), 0, rect().right(), 0); - if (experimental_mode) { - gradient.setColorAt(0, QColor(255, 155, 63, pressed ? 0xcc : 0xff)); - gradient.setColorAt(1, QColor(219, 56, 34, pressed ? 0xcc : 0xff)); - } else { - gradient.setColorAt(0, QColor(20, 255, 171, pressed ? 0xcc : 0xff)); - gradient.setColorAt(1, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); - } - p.fillPath(path, gradient); - - // vertical line - p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine)); - int line_x = rect().right() - img_width - (2 * horizontal_padding); - p.drawLine(line_x, rect().bottom(), line_x, rect().top()); -} - -void ExperimentalModeButton::showEvent(QShowEvent *event) { - experimental_mode = params.getBool("ExperimentalMode"); - mode_icon->setPixmap(experimental_mode ? experimental_pixmap : chill_pixmap); - mode_label->setText(experimental_mode ? tr("EXPERIMENTAL MODE ON") : tr("CHILL MODE ON")); -} diff --git a/selfdrive/ui/qt/offroad/experimental_mode.h b/selfdrive/ui/qt/offroad/experimental_mode.h deleted file mode 100644 index bfb7638bbe..0000000000 --- a/selfdrive/ui/qt/offroad/experimental_mode.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -#include "common/params.h" - -class ExperimentalModeButton : public QPushButton { - Q_OBJECT - -public: - explicit ExperimentalModeButton(QWidget* parent = 0); - -signals: - void openSettings(int index = 0, const QString &toggle = ""); - -private: - void showEvent(QShowEvent *event) override; - - Params params; - bool experimental_mode; - int img_width = 100; - int horizontal_padding = 30; - QPixmap experimental_pixmap; - QPixmap chill_pixmap; - QLabel *mode_label; - QLabel *mode_icon; - -protected: - void paintEvent(QPaintEvent *event) override; -}; diff --git a/selfdrive/ui/qt/offroad/firehose.cc b/selfdrive/ui/qt/offroad/firehose.cc deleted file mode 100644 index 17df440c36..0000000000 --- a/selfdrive/ui/qt/offroad/firehose.cc +++ /dev/null @@ -1,114 +0,0 @@ -#include "selfdrive/ui/qt/offroad/firehose.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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); - layout->setSpacing(20); - - // header - QLabel *title = new QLabel(tr("Firehose Mode")); - title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';"); - layout->addWidget(title, 0, Qt::AlignCenter); - - // Create a container for the content - QFrame *content = new QFrame(); - content->setStyleSheet("background-color: #292929; border-radius: 15px; padding: 20px;"); - QVBoxLayout *content_layout = new QVBoxLayout(content); - content_layout->setSpacing(20); - - // Top description - 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); - - // Add a separator - QFrame *line = new QFrame(); - line->setFrameShape(QFrame::HLine); - line->setFrameShadow(QFrame::Sunken); - line->setStyleSheet("background-color: #444444; margin-top: 5px; margin-bottom: 5px;"); - content_layout->addWidget(line); - - toggle_label = new QLabel(tr("Firehose Mode: ACTIVE")); - toggle_label->setStyleSheet("font-size: 60px; font-weight: bold; color: white;"); - content_layout->addWidget(toggle_label); - - // Add contribution label - contribution_label = new QLabel(); - contribution_label->setStyleSheet("font-size: 52px; margin-top: 10px; margin-bottom: 10px;"); - contribution_label->setWordWrap(true); - contribution_label->hide(); - content_layout->addWidget(contribution_label); - - // Add a separator before detailed instructions - QFrame *line2 = new QFrame(); - line2->setFrameShape(QFrame::HLine); - line2->setFrameShadow(QFrame::Sunken); - line2->setStyleSheet("background-color: #444444; margin-top: 10px; margin-bottom: 10px;"); - content_layout->addWidget(line2); - - // Detailed instructions at the bottom - detailed_instructions = new QLabel(tr( - "For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.
" - "
" - "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.
" - "

" - "Frequently Asked Questions

" - "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 sunnypilot (and particular forks) are able to be used for training." - )); - detailed_instructions->setStyleSheet("font-size: 40px; color: #E4E4E4;"); - detailed_instructions->setWordWrap(true); - content_layout->addWidget(detailed_instructions); - - layout->addWidget(content, 1); - - // Set up the API request for firehose stats - const QString dongle_id = QString::fromStdString(Params().get("DongleId")); - firehose_stats = new RequestRepeater(this, CommaApi::BASE_URL + "/v1/devices/" + dongle_id + "/firehose_stats", - "ApiCache_FirehoseStats", 30, true); - QObject::connect(firehose_stats, &RequestRepeater::requestDone, [=](const QString &response, bool success) { - if (success) { - QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); - QJsonObject json = doc.object(); - int count = json["firehose"].toInt(); - contribution_label->setText(tr("%n segment(s) of your driving is in the training dataset so far.", "", count)); - contribution_label->show(); - } - }); - - QObject::connect(uiState(), &UIState::uiUpdate, this, &FirehosePanel::refresh); -} - -void FirehosePanel::refresh() { - auto deviceState = (*uiState()->sm)["deviceState"].getDeviceState(); - auto networkType = deviceState.getNetworkType(); - bool networkMetered = deviceState.getNetworkMetered(); - - bool is_active = !networkMetered && (networkType != cereal::DeviceState::NetworkType::NONE); - if (is_active) { - toggle_label->setText(tr("ACTIVE")); - toggle_label->setStyleSheet("font-size: 60px; font-weight: bold; color: #2ecc71;"); - } else { - toggle_label->setText(tr("INACTIVE: connect to an unmetered network")); - toggle_label->setStyleSheet("font-size: 60px;"); - } -} diff --git a/selfdrive/ui/qt/offroad/firehose.h b/selfdrive/ui/qt/offroad/firehose.h deleted file mode 100644 index 0a5fd77ba3..0000000000 --- a/selfdrive/ui/qt/offroad/firehose.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include -#include "selfdrive/ui/qt/request_repeater.h" - -#ifdef SUNNYPILOT -#include "selfdrive/ui/sunnypilot/ui.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#else -#include "selfdrive/ui/ui.h" -#include "selfdrive/ui/qt/widgets/controls.h" -#include "selfdrive/ui/qt/offroad/settings.h" -#endif - -// Forward declarations -class SettingsWindow; - -class FirehosePanel : public QWidget { - Q_OBJECT -public: - explicit FirehosePanel(SettingsWindow *parent); - -private: - QVBoxLayout *layout; - - QLabel *detailed_instructions; - QLabel *contribution_label; - QLabel *toggle_label; - - RequestRepeater *firehose_stats; - -private slots: - void refresh(); -}; diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc deleted file mode 100644 index 309a73b939..0000000000 --- a/selfdrive/ui/qt/offroad/onboarding.cc +++ /dev/null @@ -1,211 +0,0 @@ -#include "selfdrive/ui/qt/offroad/onboarding.h" - -#include - -#include -#include -#include -#include - -#include "common/util.h" -#include "common/params.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/input.h" - -TrainingGuide::TrainingGuide(QWidget *parent) : QFrame(parent) { - setAttribute(Qt::WA_OpaquePaintEvent); -} - -void TrainingGuide::mouseReleaseEvent(QMouseEvent *e) { - if (click_timer.elapsed() < 250) { - return; - } - click_timer.restart(); - - auto contains = [this](QRect r, const QPoint &pt) { - if (image.size() != image_raw_size) { - QTransform transform; - transform.translate((width()- image.width()) / 2.0, (height()- image.height()) / 2.0); - transform.scale(image.width() / (float)image_raw_size.width(), image.height() / (float)image_raw_size.height()); - r= transform.mapRect(r); - } - return r.contains(pt); - }; - - if (contains(boundingRect[currentIndex], e->pos())) { - if (currentIndex == 9) { - const QRect yes = QRect(707, 804, 531, 164); - Params().putBool("RecordFront", contains(yes, e->pos())); - } - currentIndex += 1; - } else if (currentIndex == (boundingRect.size() - 2) && contains(boundingRect.last(), e->pos())) { - currentIndex = 0; - } - - if (currentIndex >= (boundingRect.size() - 1)) { - emit completedTraining(); - } else { - update(); - } -} - -void TrainingGuide::showEvent(QShowEvent *event) { - currentIndex = 0; - click_timer.start(); -} - -QImage TrainingGuide::loadImage(int id) { - QImage img(img_path + QString("step%1.png").arg(id)); - image_raw_size = img.size(); - if (image_raw_size != rect().size()) { - img = img.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); - } - return img; -} - -void TrainingGuide::paintEvent(QPaintEvent *event) { - QPainter painter(this); - - QRect bg(0, 0, painter.device()->width(), painter.device()->height()); - painter.fillRect(bg, QColor("#000000")); - - image = loadImage(currentIndex); - QRect rect(image.rect()); - rect.moveCenter(bg.center()); - painter.drawImage(rect.topLeft(), image); - - // progress bar - if (currentIndex > 0 && currentIndex < (boundingRect.size() - 2)) { - const int h = 20; - const int w = (currentIndex / (float)(boundingRect.size() - 2)) * width(); - painter.fillRect(QRect(0, height() - h, w, h), QColor("#465BEA")); - } -} - -void TermsPage::showEvent(QShowEvent *event) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(45, 35, 45, 45); - main_layout->setSpacing(0); - - QVBoxLayout *vlayout = new QVBoxLayout(); - vlayout->setContentsMargins(165, 165, 165, 0); - main_layout->addLayout(vlayout); - - 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 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); - - vlayout->addStretch(); - - 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("Agree")); - accept_btn->setStyleSheet(R"( - QPushButton { - background-color: #465BEA; - } - QPushButton:pressed { - background-color: #3049F4; - } - )"); - buttons->addWidget(accept_btn); - QObject::connect(accept_btn, &QPushButton::clicked, this, &TermsPage::acceptedTerms); -} - -void DeclinePage::showEvent(QShowEvent *event) { - if (layout()) { - return; - } - - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setMargin(45); - main_layout->setSpacing(40); - - QLabel *text = new QLabel(this); - 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); - - QHBoxLayout* buttons = new QHBoxLayout; - buttons->setSpacing(45); - main_layout->addLayout(buttons); - - QPushButton *back_btn = new QPushButton(tr("Back")); - buttons->addWidget(back_btn); - - QObject::connect(back_btn, &QPushButton::clicked, this, &DeclinePage::getBack); - - QPushButton *uninstall_btn = new QPushButton(tr("Decline, uninstall %1").arg(getBrand())); - uninstall_btn->setStyleSheet("background-color: #B73D3D"); - buttons->addWidget(uninstall_btn); - QObject::connect(uninstall_btn, &QPushButton::clicked, [=]() { - Params().putBool("DoUninstall", true); - }); -} - -void OnboardingWindow::updateActiveScreen() { - if (!accepted_terms) { - setCurrentIndex(0); - } else if (!training_done) { - setCurrentIndex(1); - } else { - emit onboardingDone(); - } -} - -OnboardingWindow::OnboardingWindow(QWidget *parent) : QStackedWidget(parent) { - std::string current_terms_version = params.get("TermsVersion"); - std::string current_training_version = params.get("TrainingVersion"); - accepted_terms = params.get("HasAcceptedTerms") == current_terms_version; - training_done = params.get("CompletedTrainingVersion") == current_training_version; - - TermsPage* terms = new TermsPage(this); - addWidget(terms); - connect(terms, &TermsPage::acceptedTerms, [=]() { - params.put("HasAcceptedTerms", current_terms_version); - accepted_terms = true; - updateActiveScreen(); - }); - connect(terms, &TermsPage::declinedTerms, [=]() { setCurrentIndex(2); }); - - TrainingGuide* tr = new TrainingGuide(this); - addWidget(tr); - connect(tr, &TrainingGuide::completedTraining, [=]() { - training_done = true; - params.put("CompletedTrainingVersion", current_training_version); - updateActiveScreen(); - }); - - DeclinePage* declinePage = new DeclinePage(this); - addWidget(declinePage); - connect(declinePage, &DeclinePage::getBack, [=]() { updateActiveScreen(); }); - - setStyleSheet(R"( - * { - color: white; - background-color: black; - } - QPushButton { - height: 160px; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #4F4F4F; - } - )"); - updateActiveScreen(); -} diff --git a/selfdrive/ui/qt/offroad/onboarding.h b/selfdrive/ui/qt/offroad/onboarding.h deleted file mode 100644 index 0c6c58e4a7..0000000000 --- a/selfdrive/ui/qt/offroad/onboarding.h +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "common/params.h" -#include "selfdrive/ui/qt/qt_window.h" - -class TrainingGuide : public QFrame { - Q_OBJECT - -public: - explicit TrainingGuide(QWidget *parent = 0); - -private: - void showEvent(QShowEvent *event) override; - void paintEvent(QPaintEvent *event) override; - void mouseReleaseEvent(QMouseEvent* e) override; - QImage loadImage(int id); - - QImage image; - QSize image_raw_size; - int currentIndex = 0; - - // Bounding boxes for each training guide step - const QRect continueBtn = {1840, 0, 320, 1080}; - QVector boundingRect { - QRect(112, 804, 618, 164), - continueBtn, - continueBtn, - QRect(1641, 558, 210, 313), - QRect(1662, 528, 184, 108), - continueBtn, - QRect(1814, 621, 211, 170), - QRect(1350, 0, 497, 755), - QRect(1540, 386, 468, 238), - QRect(112, 804, 1126, 164), - QRect(1598, 199, 316, 333), - continueBtn, - QRect(1364, 90, 796, 990), - continueBtn, - QRect(1593, 114, 318, 853), - QRect(1379, 511, 391, 243), - continueBtn, - continueBtn, - QRect(630, 804, 626, 164), - QRect(108, 804, 426, 164), - }; - - const QString img_path = "../assets/training/"; - QElapsedTimer click_timer; - -signals: - void completedTraining(); -}; - - -class TermsPage : public QFrame { - Q_OBJECT - -public: - explicit TermsPage(QWidget *parent = 0) : QFrame(parent) {} - -private: - void showEvent(QShowEvent *event) override; - -protected: - QPushButton *accept_btn; - -signals: - void acceptedTerms(); - void declinedTerms(); -}; - -class DeclinePage : public QFrame { - Q_OBJECT - -public: - explicit DeclinePage(QWidget *parent = 0) : QFrame(parent) {} - -private: - void showEvent(QShowEvent *event) override; - -signals: - void getBack(); -}; - -class OnboardingWindow : public QStackedWidget { - Q_OBJECT - -public: - explicit OnboardingWindow(QWidget *parent = 0); - inline void showTrainingGuide() { setCurrentIndex(1); } - virtual inline bool completed() const { return accepted_terms && training_done; } - -protected: - virtual void updateActiveScreen(); - - Params params; - bool accepted_terms = false, training_done = false; - -signals: - void onboardingDone(); -}; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc deleted file mode 100644 index 9a909ff2d2..0000000000 --- a/selfdrive/ui/qt/offroad/settings.cc +++ /dev/null @@ -1,551 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -#include "common/watchdog.h" -#include "common/util.h" -#include "selfdrive/ui/qt/network/networking.h" -#include "selfdrive/ui/qt/offroad/settings.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/widgets/prime.h" -#include "selfdrive/ui/qt/widgets/scrollview.h" -#include "selfdrive/ui/qt/offroad/developer_panel.h" -#include "selfdrive/ui/qt/offroad/firehose.h" - -TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { - // param, title, desc, icon, restart needed - std::vector> toggle_defs{ - { - "OpenpilotEnabledToggle", - 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."), - "../assets/icons/chffr_wheel.png", - true, - }, - { - "ExperimentalMode", - tr("Experimental Mode"), - "", - "../assets/icons/experimental_white.svg", - false, - }, - { - "DisengageOnAccelerator", - tr("Disengage on Accelerator Pedal"), - tr("When enabled, pressing the accelerator pedal will disengage sunnypilot."), - "../assets/icons/disengage_on_accelerator.svg", - false, - }, - { - "IsLdwEnabled", - tr("Enable Lane Departure Warnings"), - tr("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)."), - "../assets/icons/warning.png", - false, - }, - { - "AlwaysOnDM", - tr("Always-On Driver Monitoring"), - tr("Enable driver monitoring even when sunnypilot is not engaged."), - "../assets/icons/monitoring.png", - false, - }, - { - "RecordFront", - tr("Record and Upload Driver Camera"), - tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), - "../assets/icons/monitoring.png", - true, - }, - { - "RecordAudio", - tr("Record and Upload Microphone Audio"), - tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), - "../assets/icons/microphone.png", - true, - }, - { - "IsMetric", - tr("Use Metric System"), - tr("Display speed in km/h instead of mph."), - "../assets/icons/metric.png", - false, - }, - }; - - - 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, 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/icons/speed_limit.png", - longi_button_texts); - - // set up uiState update for personality setting - QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState); - - for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { - auto toggle = new ParamControl(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, &ParamControl::toggleFlipped, [=](bool state) { - params.putBool("OnroadCycleRequested", true); - }); - } - - addItem(toggle); - toggles[param.toStdString()] = toggle; - - // insert longitudinal personality after NDOG toggle - if (param == "DisengageOnAccelerator") { - addItem(long_personality_setting); - } - } - - // Toggles with confirmation dialogs -#ifndef SUNNYPILOT - toggles["ExperimentalMode"]->setActiveIcon("../assets/icons/experimental.svg"); -#endif - toggles["ExperimentalMode"]->setConfirmation(true, true); -} - -void TogglesPanel::updateState(const UIState &s) { - const SubMaster &sm = *(s.sm); - - if (sm.updated("selfdriveState")) { - auto personality = sm["selfdriveState"].getSelfdriveState().getPersonality(); - if (personality != s.scene.personality && s.scene.started && isVisible()) { - long_personality_setting->setCheckedButton(static_cast(personality)); - } - uiState()->scene.personality = personality; - } -} - -void TogglesPanel::expandToggleDescription(const QString ¶m) { - toggles[param.toStdString()]->showDescription(); -} - -void TogglesPanel::scrollToToggle(const QString ¶m) { - if (auto it = toggles.find(param.toStdString()); it != toggles.end()) { - auto scroll_area = qobject_cast(parent()->parent()); - if (scroll_area) { - scroll_area->ensureWidgetVisible(it->second); - } - } -} - -void TogglesPanel::showEvent(QShowEvent *event) { - updateToggles(); -} - -void TogglesPanel::updateToggles() { - auto experimental_mode_toggle = toggles["ExperimentalMode"]; - const QString e2e_description = QString("%1
" - "

%2


" - "%3
" - "

%4


" - "%5
") - .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. 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")) - .arg(tr("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.")); - - const bool is_release = params.getBool("IsReleaseBranch"); - 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 (hasLongitudinalControl(CP)) { - // normal description and toggle - experimental_mode_toggle->setEnabled(true); - experimental_mode_toggle->setDescription(e2e_description); - long_personality_setting->setEnabled(true); - } else { - // no long for now - experimental_mode_toggle->setEnabled(false); - long_personality_setting->setEnabled(false); - params.remove("ExperimentalMode"); - - 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("sunnypilot longitudinal control may come in a future update."); - if (CP.getAlphaLongitudinalAvailable()) { - if (is_release) { - 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 sunnypilot longitudinal control (alpha) toggle to allow Experimental mode."); - } - } - experimental_mode_toggle->setDescription("" + long_desc + "

" + e2e_description); - } - - experimental_mode_toggle->refresh(); - } else { - experimental_mode_toggle->setDescription(e2e_description); - } -} - -DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { - setSpacing(50); - addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A")))); - addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); - - pair_device = new ButtonControl(tr("Pair Device"), tr("PAIR"), - tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); - connect(pair_device, &ButtonControl::clicked, [=]() { - PairingPopup popup(this); - popup.exec(); - }); - 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"), - tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)")); - connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); - addItem(dcamBtn); -#endif - - resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); - connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); - connect(resetCalibBtn, &ButtonControl::clicked, [&]() { - if (!uiState()->engaged()) { - if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { - // Check engaged again in case it changed while the dialog was open - if (!uiState()->engaged()) { - params.remove("CalibrationParams"); - params.remove("LiveTorqueParameters"); - params.remove("LiveParameters"); - params.remove("LiveParametersV2"); - params.remove("LiveDelay"); - params.putBool("OnroadCycleRequested", true); - updateCalibDescription(); - } - } - } else { - ConfirmationDialog::alert(tr("Disengage to Reset Calibration"), this); - } - }); - addItem(resetCalibBtn); - -#ifndef SUNNYPILOT - 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(); - } - }); - addItem(retrainingBtn); - - if (Hardware::TICI()) { - auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), ""); - connect(regulatoryBtn, &ButtonControl::clicked, [=]() { - const std::string txt = util::read_file("../assets/offroad/fcc.html"); - ConfirmationDialog::rich(QString::fromStdString(txt), this); - }); - addItem(regulatoryBtn); - } - - auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), ""); - connect(translateBtn, &ButtonControl::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(translateBtn); -#endif - - QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { - for (auto btn : findChildren()) { - if (btn != pair_device && btn != resetCalibBtn) { - btn->setEnabled(offroad); - } - } - }); - -#ifndef SUNNYPILOT - // power buttons - QHBoxLayout *power_layout = new QHBoxLayout(); - power_layout->setSpacing(30); - - QPushButton *reboot_btn = new QPushButton(tr("Reboot")); - reboot_btn->setObjectName("reboot_btn"); - power_layout->addWidget(reboot_btn); - QObject::connect(reboot_btn, &QPushButton::clicked, this, &DevicePanel::reboot); - - QPushButton *poweroff_btn = new QPushButton(tr("Power Off")); - poweroff_btn->setObjectName("poweroff_btn"); - power_layout->addWidget(poweroff_btn); - QObject::connect(poweroff_btn, &QPushButton::clicked, this, &DevicePanel::poweroff); - - if (!Hardware::PC()) { - connect(uiState(), &UIState::offroadTransition, poweroff_btn, &QPushButton::setVisible); - } - - setStyleSheet(R"( - #reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; } - #reboot_btn:pressed { background-color: #4a4a4a; } - #poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; } - #poweroff_btn:pressed { background-color: #FF2424; } - )"); - addItem(power_layout); -#endif -} - -void DevicePanel::updateCalibDescription() { - QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."); - std::string calib_bytes = params.get("CalibrationParams"); - if (!calib_bytes.empty()) { - try { - AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size())); - auto calib = cmsg.getRoot().getLiveCalibration(); - if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) { - double pitch = calib.getRpyCalib()[1] * (180 / M_PI); - double yaw = calib.getRpyCalib()[2] * (180 / M_PI); - desc += tr(" Your device is pointed %1° %2 and %3° %4.") - .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), - QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); - } - } catch (kj::Exception) { - qInfo() << "invalid CalibrationParams"; - } - } - - int lag_perc = 0; - std::string lag_bytes = params.get("LiveDelay"); - if (!lag_bytes.empty()) { - try { - AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); - lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); - } catch (kj::Exception) { - qInfo() << "invalid LiveDelay"; - } - } - if (lag_perc < 100) { - desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc); - } else { - desc += tr("\n\nSteering lag calibration is complete."); - } - - std::string torque_bytes = params.get("LiveTorqueParameters"); - if (!torque_bytes.empty()) { - try { - AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size())); - auto torque = cmsg.getRoot().getLiveTorqueParameters(); - // don't add for non-torque cars - if (torque.getUseParams()) { - int torque_perc = torque.getCalPerc(); - if (torque_perc < 100) { - desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); - } else { - desc += tr(" Steering torque response calibration is complete."); - } - } - } catch (kj::Exception) { - qInfo() << "invalid LiveTorqueParameters"; - } - } - - desc += "\n\n"; - desc += tr("openpilot is continuously calibrating, resetting is rarely required. " - "Resetting calibration will restart openpilot if the car is powered on."); - resetCalibBtn->setDescription(desc); -} - -void DevicePanel::reboot() { - if (!uiState()->engaged()) { - if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), tr("Reboot"), this)) { - // Check engaged again in case it changed while the dialog was open - if (!uiState()->engaged()) { - params.putBool("DoReboot", true); - } - } - } else { - ConfirmationDialog::alert(tr("Disengage to Reboot"), this); - } -} - -void DevicePanel::poweroff() { - if (!uiState()->engaged()) { - if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), tr("Power Off"), this)) { - // Check engaged again in case it changed while the dialog was open - if (!uiState()->engaged()) { - params.putBool("DoShutdown", true); - } - } - } else { - ConfirmationDialog::alert(tr("Disengage to Power Off"), this); - } -} - -void SettingsWindow::showEvent(QShowEvent *event) { - setCurrentPanel(0); -} - -void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { - if (!param.isEmpty()) { - // Check if param ends with "Panel" to determine if it's a panel name - if (param.endsWith("Panel")) { - QString panelName = param; - panelName.chop(5); // Remove "Panel" suffix - - // Find the panel by name - for (int i = 0; i < nav_btns->buttons().size(); i++) { - 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; - } - } - } else { - emit expandToggleDescription(param); - emit scrollToToggle(param); - } - } - - panel_widget->setCurrentIndex(index); - nav_btns->buttons()[index]->setChecked(true); -} - -SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { -#ifndef SUNNYPILOT - // setup two main layouts - sidebar_widget = new QWidget; - QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); - panel_widget = new QStackedWidget(); - - // close button - QPushButton *close_btn = new QPushButton(tr("×")); - close_btn->setStyleSheet(R"( - QPushButton { - font-size: 140px; - padding-bottom: 20px; - border-radius: 100px; - background-color: #292929; - font-weight: 400; - } - QPushButton:pressed { - background-color: #3B3B3B; - } - )"); - close_btn->setFixedSize(200, 200); - sidebar_layout->addSpacing(45); - sidebar_layout->addWidget(close_btn, 0, Qt::AlignCenter); - QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); - - // setup panels - DevicePanel *device = new DevicePanel(this); - QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); - QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView); - - TogglesPanel *toggles = new TogglesPanel(this); - QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); - QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle); - - auto networking = new Networking(this); - QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); - - QList> panels = { - {tr("Device"), device}, - {tr("Network"), networking}, - {tr("Toggles"), toggles}, - {tr("Software"), new SoftwarePanel(this)}, - {tr("Firehose"), new FirehosePanel(this)}, - {tr("Developer"), new DeveloperPanel(this)}, - }; - - nav_btns = new QButtonGroup(this); - for (auto &[name, panel] : panels) { - QPushButton *btn = new QPushButton(name); - btn->setCheckable(true); - btn->setChecked(nav_btns->buttons().size() == 0); - btn->setStyleSheet(R"( - QPushButton { - color: grey; - border: none; - background: none; - font-size: 65px; - font-weight: 500; - } - QPushButton:checked { - color: white; - } - QPushButton:pressed { - color: #ADADAD; - } - )"); - btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - nav_btns->addButton(btn); - sidebar_layout->addWidget(btn, 0, Qt::AlignRight); - - const int lr_margin = name != tr("Network") ? 50 : 0; // Network panel handles its own margins - panel->setContentsMargins(lr_margin, 25, lr_margin, 25); - - ScrollView *panel_frame = new ScrollView(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, 100, 50); - - // main settings layout, sidebar + main panel - QHBoxLayout *main_layout = new QHBoxLayout(this); - - 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, ScrollView { - background-color: #292929; - border-radius: 30px; - } - )"); -#endif -} diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h deleted file mode 100644 index a11a300128..0000000000 --- a/selfdrive/ui/qt/offroad/settings.h +++ /dev/null @@ -1,118 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include - -#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 { - Q_OBJECT - -public: - explicit SettingsWindow(QWidget *parent = 0); - void setCurrentPanel(int index, const QString ¶m = ""); - -protected: - void showEvent(QShowEvent *event) override; - -signals: - void closeSettings(); - void reviewTrainingGuide(); - void showDriverView(); - void expandToggleDescription(const QString ¶m); - void scrollToToggle(const QString ¶m); - -protected: - QPushButton *sidebar_alert_widget; - QWidget *sidebar_widget; - QButtonGroup *nav_btns; - QStackedWidget *panel_widget; -}; - -class DevicePanel : public ListWidget { - Q_OBJECT -public: - explicit DevicePanel(SettingsWindow *parent); - -signals: - void reviewTrainingGuide(); - void showDriverView(); - -protected slots: - void poweroff(); - void reboot(); - void updateCalibDescription(); - -protected: - Params params; - ButtonControl *pair_device; - ButtonControl *resetCalibBtn; -}; - -class TogglesPanel : public ListWidget { - Q_OBJECT -public: - explicit TogglesPanel(SettingsWindow *parent); - void showEvent(QShowEvent *event) override; - -public slots: - void expandToggleDescription(const QString ¶m); - void scrollToToggle(const QString ¶m); - -protected slots: - virtual void updateState(const UIState &s); - -protected: - Params params; - std::map toggles; - ButtonParamControl *long_personality_setting; - - virtual void updateToggles(); -}; - -class SoftwarePanel : public ListWidget { - Q_OBJECT -public: - explicit SoftwarePanel(QWidget* parent = nullptr); - -protected: - void showEvent(QShowEvent *event) override; - virtual void updateLabels(); - void checkForUpdates(); - - bool is_onroad = false; - - QLabel *onroadLbl; - LabelControl *versionLbl; - ButtonControl *installBtn; - ButtonControl *downloadBtn; - ButtonControl *targetBranchBtn; - - Params params; - ParamWatcher *fs_watch; -}; - -// Forward declaration -class FirehosePanel; diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc deleted file mode 100644 index 449f3c5203..0000000000 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ /dev/null @@ -1,154 +0,0 @@ -#include "selfdrive/ui/qt/offroad/settings.h" - -#include -#include -#include - -#include -#include - -#include "common/params.h" -#include "common/util.h" -#include "selfdrive/ui/ui.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/controls.h" -#include "selfdrive/ui/qt/widgets/input.h" -#include "system/hardware/hw.h" - - -void SoftwarePanel::checkForUpdates() { - std::system("pkill -SIGUSR1 -f system.updated.updated"); -} - -SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { - onroadLbl = new QLabel(tr("Updates are only downloaded while the car is off.")); - onroadLbl->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; padding-top: 30px; padding-bottom: 30px;"); - addItem(onroadLbl); - - // current version - versionLbl = new LabelControl(tr("Current Version"), ""); - addItem(versionLbl); - - // download update btn - downloadBtn = new ButtonControl(tr("Download"), tr("CHECK")); - connect(downloadBtn, &ButtonControl::clicked, [=]() { - downloadBtn->setEnabled(false); - if (downloadBtn->text() == tr("CHECK")) { - checkForUpdates(); - } else { - std::system("pkill -SIGHUP -f system.updated.updated"); - } - }); - addItem(downloadBtn); - - // install update btn - installBtn = new ButtonControl(tr("Install Update"), tr("INSTALL")); - connect(installBtn, &ButtonControl::clicked, [=]() { - installBtn->setEnabled(false); - params.putBool("DoReboot", true); - }); - addItem(installBtn); - - // branch selecting - targetBranchBtn = new ButtonControl(tr("Target Branch"), tr("SELECT")); - connect(targetBranchBtn, &ButtonControl::clicked, [=]() { - auto current = params.get("GitBranch"); - QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); - for (QString b : {current.c_str(), "devel-staging", "devel", "nightly", "nightly-dev", "master"}) { - auto i = branches.indexOf(b); - if (i >= 0) { - branches.removeAt(i); - branches.insert(0, b); - } - } - - QString cur = QString::fromStdString(params.get("UpdaterTargetBranch")); - QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this); - if (!selection.isEmpty()) { - params.put("UpdaterTargetBranch", selection.toStdString()); - targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); - checkForUpdates(); - } - }); - addItem(targetBranchBtn); - - // uninstall button - auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); - connect(uninstallBtn, &ButtonControl::clicked, [&]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), tr("Uninstall"), this)) { - params.putBool("DoUninstall", true); - } - }); - addItem(uninstallBtn); - - fs_watch = new ParamWatcher(this); - QObject::connect(fs_watch, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { - updateLabels(); - }); - - connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { - is_onroad = !offroad; - updateLabels(); - }); - - updateLabels(); -} - -void SoftwarePanel::showEvent(QShowEvent *event) { - // nice for testing on PC - installBtn->setEnabled(true); - - updateLabels(); -} - -void SoftwarePanel::updateLabels() { - // add these back in case the files got removed - fs_watch->addParam("LastUpdateTime"); - fs_watch->addParam("UpdateFailedCount"); - fs_watch->addParam("UpdaterState"); - fs_watch->addParam("UpdateAvailable"); - - if (!isVisible()) { - return; - } - - // updater only runs offroad - onroadLbl->setVisible(is_onroad); - downloadBtn->setVisible(!is_onroad); - - // download update - QString updater_state = QString::fromStdString(params.get("UpdaterState")); - bool failed = std::atoi(params.get("UpdateFailedCount").c_str()) > 0; - if (updater_state != "idle") { - downloadBtn->setEnabled(false); - downloadBtn->setValue(updater_state); - } else { - if (failed) { - downloadBtn->setText(tr("CHECK")); - downloadBtn->setValue(tr("failed to check for update")); - } else if (params.getBool("UpdaterFetchAvailable")) { - downloadBtn->setText(tr("DOWNLOAD")); - downloadBtn->setValue(tr("update available")); - } else { - QString lastUpdate = tr("never"); - auto tm = params.get("LastUpdateTime"); - if (!tm.empty()) { - lastUpdate = timeAgo(QDateTime::fromString(QString::fromStdString(tm + "Z"), Qt::ISODate)); - } - downloadBtn->setText(tr("CHECK")); - downloadBtn->setValue(tr("up to date, last checked %1").arg(lastUpdate)); - } - downloadBtn->setEnabled(true); - } - targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); - - // current + new versions - versionLbl->setText(QString::fromStdString(params.get("UpdaterCurrentDescription"))); - versionLbl->setDescription(QString::fromStdString(params.get("UpdaterCurrentReleaseNotes"))); - - installBtn->setVisible(!is_onroad && params.getBool("UpdateAvailable")); - installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription"))); - installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes"))); - - update(); -} diff --git a/selfdrive/ui/qt/onroad/alerts.cc b/selfdrive/ui/qt/onroad/alerts.cc deleted file mode 100644 index d6829c6b08..0000000000 --- a/selfdrive/ui/qt/onroad/alerts.cc +++ /dev/null @@ -1,112 +0,0 @@ -#include "selfdrive/ui/qt/onroad/alerts.h" - -#include -#include - -#include "selfdrive/ui/qt/util.h" - -void OnroadAlerts::updateState(const UIState &s) { - Alert a = getAlert(*(s.sm), s.scene.started_frame); - if (!alert.equal(a)) { - alert = a; - update(); - } -} - -void OnroadAlerts::clear() { - alert = {}; - update(); -} - -OnroadAlerts::Alert OnroadAlerts::getAlert(const SubMaster &sm, uint64_t started_frame) { - const cereal::SelfdriveState::Reader &ss = sm["selfdriveState"].getSelfdriveState(); - const uint64_t selfdrive_frame = sm.rcv_frame("selfdriveState"); - - Alert a = {}; - if (selfdrive_frame >= started_frame) { // Don't get old alert. - a = {ss.getAlertText1().cStr(), ss.getAlertText2().cStr(), - ss.getAlertType().cStr(), ss.getAlertSize(), ss.getAlertStatus()}; - } - - if (!sm.updated("selfdriveState") && (sm.frame - started_frame) > 5 * UI_FREQ) { - const int SELFDRIVE_STATE_TIMEOUT = 5; - const int ss_missing = (nanos_since_boot() - sm.rcv_time("selfdriveState")) / 1e9; - - // Handle selfdrive timeout - if (selfdrive_frame < started_frame) { - // car is started, but selfdriveState hasn't been seen at all - 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()) { - // car is started, but selfdrive is lagging or died - if (ss.getEnabled() && (ss_missing - SELFDRIVE_STATE_TIMEOUT) < 10) { - a = {tr("TAKE CONTROL IMMEDIATELY"), tr("System Unresponsive"), - "selfdriveUnresponsive", cereal::SelfdriveState::AlertSize::FULL, - cereal::SelfdriveState::AlertStatus::CRITICAL}; - } else { - a = {tr("System Unresponsive"), tr("Reboot Device"), - "selfdriveUnresponsivePermanent", cereal::SelfdriveState::AlertSize::MID, - cereal::SelfdriveState::AlertStatus::NORMAL}; - } - } - } - return a; -} - -void OnroadAlerts::paintEvent(QPaintEvent *event) { - if (alert.size == cereal::SelfdriveState::AlertSize::NONE) { - return; - } - static std::map alert_heights = { - {cereal::SelfdriveState::AlertSize::SMALL, 271}, - {cereal::SelfdriveState::AlertSize::MID, 420}, - {cereal::SelfdriveState::AlertSize::FULL, height()}, - }; - int h = alert_heights[alert.size]; - - int margin = 40; - int radius = 30; - if (alert.size == cereal::SelfdriveState::AlertSize::FULL) { - margin = 0; - radius = 0; - } - QRect r = QRect(0 + margin, height() - h + margin, width() - margin*2, h - margin*2); - - QPainter p(this); - - // draw background + gradient - p.setPen(Qt::NoPen); - p.setCompositionMode(QPainter::CompositionMode_SourceOver); - p.setBrush(QBrush(alert_colors[alert.status])); - p.drawRoundedRect(r, radius, radius); - - QLinearGradient g(0, r.y(), 0, r.bottom()); - g.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.05)); - g.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0.35)); - - p.setCompositionMode(QPainter::CompositionMode_DestinationOver); - p.setBrush(QBrush(g)); - p.drawRoundedRect(r, radius, radius); - p.setCompositionMode(QPainter::CompositionMode_SourceOver); - - // text - const QPoint c = r.center(); - p.setPen(QColor(0xff, 0xff, 0xff)); - p.setRenderHint(QPainter::TextAntialiasing); - if (alert.size == cereal::SelfdriveState::AlertSize::SMALL) { - p.setFont(InterFont(74, QFont::DemiBold)); - p.drawText(r, Qt::AlignCenter, alert.text1); - } else if (alert.size == cereal::SelfdriveState::AlertSize::MID) { - p.setFont(InterFont(88, QFont::Bold)); - p.drawText(QRect(0, c.y() - 125, width(), 150), Qt::AlignHCenter | Qt::AlignTop, alert.text1); - p.setFont(InterFont(66)); - p.drawText(QRect(0, c.y() + 21, width(), 90), Qt::AlignHCenter, alert.text2); - } else if (alert.size == cereal::SelfdriveState::AlertSize::FULL) { - bool l = alert.text1.length() > 15; - p.setFont(InterFont(l ? 132 : 177, QFont::Bold)); - p.drawText(QRect(0, r.y() + (l ? 240 : 270), width(), 600), Qt::AlignHCenter | Qt::TextWordWrap, alert.text1); - p.setFont(InterFont(88)); - p.drawText(QRect(0, r.height() - (l ? 361 : 420), width(), 300), Qt::AlignHCenter | Qt::TextWordWrap, alert.text2); - } -} diff --git a/selfdrive/ui/qt/onroad/alerts.h b/selfdrive/ui/qt/onroad/alerts.h deleted file mode 100644 index de38d8ffc3..0000000000 --- a/selfdrive/ui/qt/onroad/alerts.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include - -#include "selfdrive/ui/ui.h" - -class OnroadAlerts : public QWidget { - Q_OBJECT - -public: - OnroadAlerts(QWidget *parent = 0) : QWidget(parent) {} - void updateState(const UIState &s); - void clear(); - -protected: - struct Alert { - QString text1; - QString text2; - QString type; - cereal::SelfdriveState::AlertSize size; - cereal::SelfdriveState::AlertStatus status; - - bool equal(const Alert &other) const { - return text1 == other.text1 && text2 == other.text2 && type == other.type; - } - }; - - const QMap alert_colors = { - {cereal::SelfdriveState::AlertStatus::NORMAL, QColor(0x15, 0x15, 0x15, 0xf1)}, - {cereal::SelfdriveState::AlertStatus::USER_PROMPT, QColor(0xDA, 0x6F, 0x25, 0xf1)}, - {cereal::SelfdriveState::AlertStatus::CRITICAL, QColor(0xC9, 0x22, 0x31, 0xf1)}, - }; - - void paintEvent(QPaintEvent*) override; - OnroadAlerts::Alert getAlert(const SubMaster &sm, uint64_t started_frame); - - QColor bg; - Alert alert = {}; -}; diff --git a/selfdrive/ui/qt/onroad/annotated_camera.cc b/selfdrive/ui/qt/onroad/annotated_camera.cc deleted file mode 100644 index f504ad69f1..0000000000 --- a/selfdrive/ui/qt/onroad/annotated_camera.cc +++ /dev/null @@ -1,157 +0,0 @@ - -#include "selfdrive/ui/qt/onroad/annotated_camera.h" - -#include -#include -#include - -#include "common/swaglog.h" -#include "selfdrive/ui/qt/util.h" - -// Window that shows camera view and variety of info drawn on top -AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget *parent) - : fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraWidget("camerad", type, parent) { - pm = std::make_unique(std::vector{"uiDebug"}); - - main_layout = new QVBoxLayout(this); - main_layout->setMargin(UI_BORDER_SIZE); - main_layout->setSpacing(0); - - experimental_btn = new ExperimentalButton(this); - main_layout->addWidget(experimental_btn, 0, Qt::AlignTop | Qt::AlignRight); -} - -void AnnotatedCameraWidget::updateState(const UIState &s) { - // update engageability/experimental mode button - experimental_btn->updateState(s); - dmon.updateState(s); -} - -void AnnotatedCameraWidget::initializeGL() { - CameraWidget::initializeGL(); - qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION)); - qInfo() << "OpenGL vendor:" << QString((const char*)glGetString(GL_VENDOR)); - qInfo() << "OpenGL renderer:" << QString((const char*)glGetString(GL_RENDERER)); - qInfo() << "OpenGL language version:" << QString((const char*)glGetString(GL_SHADING_LANGUAGE_VERSION)); - - prev_draw_t = millis_since_boot(); - setBackgroundColor(bg_colors[STATUS_DISENGAGED]); -} - -mat4 AnnotatedCameraWidget::calcFrameMatrix() { - // Project point at "infinity" to compute x and y offsets - // to ensure this ends up in the middle of the screen - // for narrow come and a little lower for wide cam. - // TODO: use proper perspective transform? - - // Select intrinsic matrix and calibration based on camera type - auto *s = uiState(); - bool wide_cam = active_stream_type == VISION_STREAM_WIDE_ROAD; - const auto &intrinsic_matrix = wide_cam ? ECAM_INTRINSIC_MATRIX : FCAM_INTRINSIC_MATRIX; - const auto &calibration = wide_cam ? s->scene.view_from_wide_calib : s->scene.view_from_calib; - - // Compute the calibration transformation matrix - const auto calib_transform = intrinsic_matrix * calibration; - - float zoom = wide_cam ? 2.0 : 1.1; - Eigen::Vector3f inf(1000., 0., 0.); - auto Kep = calib_transform * inf; - - int w = width(), h = height(); - float center_x = intrinsic_matrix(0, 2); - float center_y = intrinsic_matrix(1, 2); - - float max_x_offset = center_x * zoom - w / 2 - 5; - float max_y_offset = center_y * zoom - h / 2 - 5; - float x_offset = std::clamp((Kep.x() / Kep.z() - center_x) * zoom, -max_x_offset, max_x_offset); - float y_offset = std::clamp((Kep.y() / Kep.z() - center_y) * zoom, -max_y_offset, max_y_offset); - - // Apply transformation such that video pixel coordinates match video - // 1) Put (0, 0) in the middle of the video - // 2) Apply same scaling as video - // 3) Put (0, 0) in top left corner of video - Eigen::Matrix3f video_transform =(Eigen::Matrix3f() << - zoom, 0.0f, (w / 2 - x_offset) - (center_x * zoom), - 0.0f, zoom, (h / 2 - y_offset) - (center_y * zoom), - 0.0f, 0.0f, 1.0f).finished(); - - model.setTransform(video_transform * calib_transform); - - float zx = zoom * 2 * center_x / w; - float zy = zoom * 2 * center_y / h; - return mat4{{ - zx, 0.0, 0.0, -x_offset / w * 2, - 0.0, zy, 0.0, y_offset / h * 2, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - }}; -} - -void AnnotatedCameraWidget::paintGL() { - UIState *s = uiState(); - SubMaster &sm = *(s->sm); - const double start_draw_t = millis_since_boot(); - - // draw camera frame - { - std::lock_guard lk(frame_lock); - - if (frames.empty()) { - if (skip_frame_count > 0) { - skip_frame_count--; - qDebug() << "skipping frame, not ready"; - return; - } - } else { - // skip drawing up to this many frames if we're - // missing camera frames. this smooths out the - // transitions from the narrow and wide cameras - skip_frame_count = 5; - } - - // Wide or narrow cam dependent on speed - bool has_wide_cam = available_streams.count(VISION_STREAM_WIDE_ROAD); - if (has_wide_cam) { - float v_ego = sm["carState"].getCarState().getVEgo(); - if ((v_ego < 10) || available_streams.size() == 1) { - wide_cam_requested = true; - } else if (v_ego > 15) { - wide_cam_requested = false; - } - wide_cam_requested = wide_cam_requested && sm["selfdriveState"].getSelfdriveState().getExperimentalMode(); - } - CameraWidget::setStreamType(wide_cam_requested ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD); - CameraWidget::setFrameId(sm["modelV2"].getModelV2().getFrameId()); - CameraWidget::paintGL(); - } - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - painter.setPen(Qt::NoPen); - - model.draw(painter, rect()); - dmon.draw(painter, rect()); - hud.updateState(*s); - hud.draw(painter, rect()); - - double cur_draw_t = millis_since_boot(); - double dt = cur_draw_t - prev_draw_t; - double fps = fps_filter.update(1. / dt * 1000); - if (fps < 15) { - LOGW("slow frame rate: %.2f fps", fps); - } - prev_draw_t = cur_draw_t; - - // publish debug msg - MessageBuilder msg; - auto m = msg.initEvent().initUiDebug(); - m.setDrawTimeMillis(cur_draw_t - start_draw_t); - pm->send("uiDebug", msg); -} - -void AnnotatedCameraWidget::showEvent(QShowEvent *event) { - CameraWidget::showEvent(event); - - ui_update_params(uiState()); - prev_draw_t = millis_since_boot(); -} diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h deleted file mode 100644 index 5d9d21ab6b..0000000000 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include -#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" -#include "selfdrive/ui/sunnypilot/qt/onroad/model.h" -#define ExperimentalButton ExperimentalButtonSP -#define ModelRenderer ModelRendererSP -#define HudRenderer HudRendererSP -#else -#include "selfdrive/ui/qt/onroad/buttons.h" -#include "selfdrive/ui/qt/onroad/hud.h" -#endif - -class AnnotatedCameraWidget : public CameraWidget { - Q_OBJECT - -public: - explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0); - virtual ~AnnotatedCameraWidget() = default; - virtual void updateState(const UIState &s); - -private: - QVBoxLayout *main_layout; - ExperimentalButton *experimental_btn; - DriverMonitorRenderer dmon; - HudRenderer hud; - ModelRenderer model; - std::unique_ptr pm; - - int skip_frame_count = 0; - bool wide_cam_requested = false; - -protected: - void paintGL() override; - void initializeGL() override; - void showEvent(QShowEvent *event) override; - mat4 calcFrameMatrix() override; - - double prev_draw_t = 0; - FirstOrderFilter fps_filter; -}; diff --git a/selfdrive/ui/qt/onroad/buttons.cc b/selfdrive/ui/qt/onroad/buttons.cc deleted file mode 100644 index 75b8a8b7e6..0000000000 --- a/selfdrive/ui/qt/onroad/buttons.cc +++ /dev/null @@ -1,53 +0,0 @@ -#include "selfdrive/ui/qt/onroad/buttons.h" - -#include - -#include "selfdrive/ui/qt/util.h" - -void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity) { - p.setRenderHint(QPainter::Antialiasing); - p.setOpacity(1.0); // bg dictates opacity of ellipse - p.setPen(Qt::NoPen); - p.setBrush(bg); - p.drawEllipse(center, btn_size / 2, btn_size / 2); - p.setOpacity(opacity); - p.drawPixmap(center - QPoint(img.width() / 2, img.height() / 2), img); - p.setOpacity(1.0); -} - -// ExperimentalButton -ExperimentalButton::ExperimentalButton(QWidget *parent) : experimental_mode(false), engageable(false), QPushButton(parent) { - setFixedSize(btn_size, btn_size); - - engage_img = loadPixmap("../assets/icons/chffr_wheel.png", {img_size, img_size}); - experimental_img = loadPixmap("../assets/icons/experimental.svg", {img_size, img_size}); - QObject::connect(this, &QPushButton::clicked, this, &ExperimentalButton::changeMode); -} - -void ExperimentalButton::changeMode() { - const auto cp = (*uiState()->sm)["carParams"].getCarParams(); - bool can_change = hasLongitudinalControl(cp) && params.getBool("ExperimentalModeConfirmed"); - if (can_change) { - params.putBool("ExperimentalMode", !experimental_mode); - } -} - -void ExperimentalButton::updateState(const UIState &s) { - const auto cs = (*s.sm)["selfdriveState"].getSelfdriveState(); - bool eng = cs.getEngageable() || cs.getEnabled(); - if ((cs.getExperimentalMode() != experimental_mode) || (eng != engageable)) { - engageable = eng; - experimental_mode = cs.getExperimentalMode(); - update(); - } -} - -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 deleted file mode 100644 index fca909f9b2..0000000000 --- a/selfdrive/ui/qt/onroad/buttons.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#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; - -class ExperimentalButton : public QPushButton { - Q_OBJECT - -public: - explicit ExperimentalButton(QWidget *parent = 0); - 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; - bool engageable; -}; - -void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity); diff --git a/selfdrive/ui/qt/onroad/driver_monitoring.cc b/selfdrive/ui/qt/onroad/driver_monitoring.cc deleted file mode 100644 index e67c483047..0000000000 --- a/selfdrive/ui/qt/onroad/driver_monitoring.cc +++ /dev/null @@ -1,112 +0,0 @@ -#include "selfdrive/ui/qt/onroad/driver_monitoring.h" -#include -#include - -#include "selfdrive/ui/qt/onroad/buttons.h" -#include "selfdrive/ui/qt/util.h" - -// Default 3D coordinates for face keypoints -static constexpr vec3 DEFAULT_FACE_KPTS_3D[] = { - {-5.98, -51.20, 8.00}, {-17.64, -49.14, 8.00}, {-23.81, -46.40, 8.00}, {-29.98, -40.91, 8.00}, {-32.04, -37.49, 8.00}, - {-34.10, -32.00, 8.00}, {-36.16, -21.03, 8.00}, {-36.16, 6.40, 8.00}, {-35.47, 10.51, 8.00}, {-32.73, 19.43, 8.00}, - {-29.30, 26.29, 8.00}, {-24.50, 33.83, 8.00}, {-19.01, 41.37, 8.00}, {-14.21, 46.17, 8.00}, {-12.16, 47.54, 8.00}, - {-4.61, 49.60, 8.00}, {4.99, 49.60, 8.00}, {12.53, 47.54, 8.00}, {14.59, 46.17, 8.00}, {19.39, 41.37, 8.00}, - {24.87, 33.83, 8.00}, {29.67, 26.29, 8.00}, {33.10, 19.43, 8.00}, {35.84, 10.51, 8.00}, {36.53, 6.40, 8.00}, - {36.53, -21.03, 8.00}, {34.47, -32.00, 8.00}, {32.42, -37.49, 8.00}, {30.36, -40.91, 8.00}, {24.19, -46.40, 8.00}, - {18.02, -49.14, 8.00}, {6.36, -51.20, 8.00}, {-5.98, -51.20, 8.00}, -}; - -// Colors used for drawing based on monitoring state -static const QColor DMON_ENGAGED_COLOR = QColor::fromRgbF(0.1, 0.945, 0.26); -static const QColor DMON_DISENGAGED_COLOR = QColor::fromRgbF(0.545, 0.545, 0.545); - -DriverMonitorRenderer::DriverMonitorRenderer() : face_kpts_draw(std::size(DEFAULT_FACE_KPTS_3D)) { - dm_img = loadPixmap("../assets/icons/driver_face.png", {img_size + 5, img_size + 5}); -} - -void DriverMonitorRenderer::updateState(const UIState &s) { - auto &sm = *(s.sm); - is_visible = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE && - sm.rcv_frame("driverStateV2") > s.scene.started_frame; - if (!is_visible) return; - - auto dm_state = sm["driverMonitoringState"].getDriverMonitoringState(); - is_active = dm_state.getIsActiveMode(); - is_rhd = dm_state.getIsRHD(); - dm_fade_state = std::clamp(dm_fade_state + 0.2f * (0.5f - is_active), 0.0f, 1.0f); - - const auto &driverstate = sm["driverStateV2"].getDriverStateV2(); - const auto driver_orient = is_rhd ? driverstate.getRightDriverData().getFaceOrientation() : driverstate.getLeftDriverData().getFaceOrientation(); - - for (int i = 0; i < 3; ++i) { - float v_this = (i == 0 ? (driver_orient[i] < 0 ? 0.7 : 0.9) : 0.4) * driver_orient[i]; - driver_pose_diff[i] = std::abs(driver_pose_vals[i] - v_this); - driver_pose_vals[i] = 0.8f * v_this + (1 - 0.8) * driver_pose_vals[i]; - driver_pose_sins[i] = std::sin(driver_pose_vals[i] * (1.0f - dm_fade_state)); - driver_pose_coss[i] = std::cos(driver_pose_vals[i] * (1.0f - dm_fade_state)); - } - - auto [sin_y, sin_x, sin_z] = driver_pose_sins; - auto [cos_y, cos_x, cos_z] = driver_pose_coss; - - // Rotation matrix for transforming face keypoints based on driver's head orientation - const mat3 r_xyz = {{ - cos_x * cos_z, cos_x * sin_z, -sin_x, - -sin_y * sin_x * cos_z - cos_y * sin_z, -sin_y * sin_x * sin_z + cos_y * cos_z, -sin_y * cos_x, - cos_y * sin_x * cos_z - sin_y * sin_z, cos_y * sin_x * sin_z + sin_y * cos_z, cos_y * cos_x, - }}; - - // Transform vertices - for (int i = 0; i < face_kpts_draw.size(); ++i) { - vec3 kpt = matvecmul3(r_xyz, DEFAULT_FACE_KPTS_3D[i]); - face_kpts_draw[i] = {{kpt.v[0], kpt.v[1], kpt.v[2] * (1.0f - dm_fade_state) + 8 * dm_fade_state}}; - } -} - -void DriverMonitorRenderer::draw(QPainter &painter, const QRect &surface_rect) { - if (!is_visible) return; - - painter.save(); - - int offset = UI_BORDER_SIZE + btn_size / 2; - float x = is_rhd ? surface_rect.width() - offset : offset; - float y = surface_rect.height() - offset; - float opacity = is_active ? 0.65f : 0.2f; - -#ifdef SUNNYPILOT - const int dev_ui_info = uiStateSP()->scene.dev_ui_info; - y -= dev_ui_info > 1 ? 50 : 0; -#endif - - drawIcon(painter, QPoint(x, y), dm_img, QColor(0, 0, 0, 70), opacity); - - QPointF keypoints[std::size(DEFAULT_FACE_KPTS_3D)]; - for (int i = 0; i < std::size(keypoints); ++i) { - const auto &v = face_kpts_draw[i].v; - float kp = (v[2] - 8) / 120.0f + 1.0f; - keypoints[i] = QPointF(v[0] * kp + x, v[1] * kp + y); - } - - painter.setPen(QPen(QColor::fromRgbF(1.0, 1.0, 1.0, opacity), 5.2, Qt::SolidLine, Qt::RoundCap)); - painter.drawPolyline(keypoints, std::size(keypoints)); - - // tracking arcs - const int arc_l = 133; - const float arc_t_default = 6.7f; - const float arc_t_extend = 12.0f; - QColor arc_color = uiState()->engaged() ? DMON_ENGAGED_COLOR : DMON_DISENGAGED_COLOR; - arc_color.setAlphaF(0.4 * (1.0f - dm_fade_state)); - - float delta_x = -driver_pose_sins[1] * arc_l / 2.0f; - float delta_y = -driver_pose_sins[0] * arc_l / 2.0f; - - // Draw horizontal tracking arc - painter.setPen(QPen(arc_color, arc_t_default + arc_t_extend * std::min(1.0, driver_pose_diff[1] * 5.0), Qt::SolidLine, Qt::RoundCap)); - painter.drawArc(QRectF(std::min(x + delta_x, x), y - arc_l / 2, std::abs(delta_x), arc_l), (driver_pose_sins[1] > 0 ? 90 : -90) * 16, 180 * 16); - - // Draw vertical tracking arc - painter.setPen(QPen(arc_color, arc_t_default + arc_t_extend * std::min(1.0, driver_pose_diff[0] * 5.0), Qt::SolidLine, Qt::RoundCap)); - painter.drawArc(QRectF(x - arc_l / 2, std::min(y + delta_y, y), arc_l, std::abs(delta_y)), (driver_pose_sins[0] > 0 ? 0 : 180) * 16, 180 * 16); - - painter.restore(); -} diff --git a/selfdrive/ui/qt/onroad/driver_monitoring.h b/selfdrive/ui/qt/onroad/driver_monitoring.h deleted file mode 100644 index 47db151c81..0000000000 --- a/selfdrive/ui/qt/onroad/driver_monitoring.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include -#include -#include "selfdrive/ui/ui.h" - -class DriverMonitorRenderer { -public: - DriverMonitorRenderer(); - void updateState(const UIState &s); - void draw(QPainter &painter, const QRect &surface_rect); - -private: - float driver_pose_vals[3] = {}; - float driver_pose_diff[3] = {}; - float driver_pose_sins[3] = {}; - float driver_pose_coss[3] = {}; - bool is_visible = false; - bool is_active = false; - bool is_rhd = false; - float dm_fade_state = 1.0; - QPixmap dm_img; - std::vector face_kpts_draw; -}; diff --git a/selfdrive/ui/qt/onroad/hud.cc b/selfdrive/ui/qt/onroad/hud.cc deleted file mode 100644 index 540643bed5..0000000000 --- a/selfdrive/ui/qt/onroad/hud.cc +++ /dev/null @@ -1,113 +0,0 @@ -#include "selfdrive/ui/qt/onroad/hud.h" - -#include - -#include "selfdrive/ui/qt/util.h" - -constexpr int SET_SPEED_NA = 255; - -HudRenderer::HudRenderer() {} - -void HudRenderer::updateState(const UIState &s) { - is_metric = s.scene.is_metric; - status = s.status; - - const SubMaster &sm = *(s.sm); - if (sm.rcv_frame("carState") < s.scene.started_frame) { - is_cruise_set = false; - set_speed = SET_SPEED_NA; - speed = 0.0; - return; - } - - const auto &controls_state = sm["controlsState"].getControlsState(); - const auto &car_state = sm["carState"].getCarState(); - - // Handle older routes where vCruiseCluster is not set - set_speed = car_state.getVCruiseCluster() == 0.0 ? controls_state.getVCruiseDEPRECATED() : car_state.getVCruiseCluster(); - is_cruise_set = set_speed > 0 && set_speed != SET_SPEED_NA; - is_cruise_available = set_speed != -1; - - if (is_cruise_set && !is_metric) { - set_speed *= KM_TO_MILE; - } - - // Handle older routes where vEgoCluster is not set - v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0; - float v_ego = v_ego_cluster_seen ? car_state.getVEgoCluster() : car_state.getVEgo(); - speed = std::max(0.0f, v_ego * (is_metric ? MS_TO_KPH : MS_TO_MPH)); -} - -void HudRenderer::draw(QPainter &p, const QRect &surface_rect) { - p.save(); - - // Draw header gradient - QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT); - bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45)); - bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0)); - p.fillRect(0, 0, surface_rect.width(), UI_HEADER_HEIGHT, bg); - -#ifndef SUNNYPILOT - if (is_cruise_available) { - drawSetSpeed(p, surface_rect); - } - drawCurrentSpeed(p, surface_rect); -#endif - - p.restore(); -} - -void HudRenderer::drawSetSpeed(QPainter &p, const QRect &surface_rect) { - // Draw outer box + border to contain set speed - const QSize default_size = {172, 204}; - QSize set_speed_size = is_metric ? QSize(200, 204) : default_size; - QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size); - - // Draw set speed box - p.setPen(QPen(QColor(255, 255, 255, 75), 6)); - p.setBrush(QColor(0, 0, 0, 166)); - p.drawRoundedRect(set_speed_rect, 32, 32); - - // Colors based on status - QColor max_color = QColor(0xa6, 0xa6, 0xa6, 0xff); - QColor set_speed_color = QColor(0x72, 0x72, 0x72, 0xff); - if (is_cruise_set) { - set_speed_color = QColor(255, 255, 255); - if (status == STATUS_DISENGAGED) { - max_color = QColor(255, 255, 255); - } else if (status == STATUS_OVERRIDE) { - max_color = QColor(0x91, 0x9b, 0x95, 0xff); - } else { - max_color = QColor(0x80, 0xd8, 0xa6, 0xff); - } - } - - // Draw "MAX" text - p.setFont(InterFont(40, QFont::DemiBold)); - p.setPen(max_color); - p.drawText(set_speed_rect.adjusted(0, 27, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("MAX")); - - // Draw set speed - QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "–"; - p.setFont(InterFont(90, QFont::Bold)); - p.setPen(set_speed_color); - p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr); -} - -void HudRenderer::drawCurrentSpeed(QPainter &p, const QRect &surface_rect) { - QString speedStr = QString::number(std::nearbyint(speed)); - - p.setFont(InterFont(176, QFont::Bold)); - drawText(p, surface_rect.center().x(), 210, speedStr); - - p.setFont(InterFont(66)); - drawText(p, surface_rect.center().x(), 290, is_metric ? tr("km/h") : tr("mph"), 200); -} - -void HudRenderer::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { - QRect real_rect = p.fontMetrics().boundingRect(text); - real_rect.moveCenter({x, y - real_rect.height() / 2}); - - p.setPen(QColor(0xff, 0xff, 0xff, alpha)); - p.drawText(real_rect.x(), real_rect.bottom(), text); -} diff --git a/selfdrive/ui/qt/onroad/hud.h b/selfdrive/ui/qt/onroad/hud.h deleted file mode 100644 index 6638af8a05..0000000000 --- a/selfdrive/ui/qt/onroad/hud.h +++ /dev/null @@ -1,32 +0,0 @@ -#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(); - virtual ~HudRenderer() = default; - virtual void updateState(const UIState &s); - virtual void draw(QPainter &p, const QRect &surface_rect); - -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); - - float speed = 0; - float set_speed = 0; - bool is_cruise_set = false; - bool is_cruise_available = true; - bool is_metric = false; - bool v_ego_cluster_seen = false; - int status = STATUS_DISENGAGED; -}; diff --git a/selfdrive/ui/qt/onroad/model.cc b/selfdrive/ui/qt/onroad/model.cc deleted file mode 100644 index 176af56613..0000000000 --- a/selfdrive/ui/qt/onroad/model.cc +++ /dev/null @@ -1,237 +0,0 @@ -#include "selfdrive/ui/qt/onroad/model.h" - -void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { - auto *s = uiState(); - auto &sm = *(s->sm); - // Check if data is up-to-date - if (sm.rcv_frame("liveCalibration") < s->scene.started_frame || - sm.rcv_frame("modelV2") < s->scene.started_frame) { - return; - } - - clip_region = surface_rect.adjusted(-CLIP_MARGIN, -CLIP_MARGIN, CLIP_MARGIN, CLIP_MARGIN); - experimental_mode = sm["selfdriveState"].getSelfdriveState().getExperimentalMode(); - longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl(); - path_offset_z = sm["liveCalibration"].getLiveCalibration().getHeight()[0]; - - painter.save(); - - const auto &model = sm["modelV2"].getModelV2(); - const auto &radar_state = sm["radarState"].getRadarState(); - const auto &lead_one = radar_state.getLeadOne(); - - update_model(model, lead_one); - drawLaneLines(painter); - drawPath(painter, model, surface_rect.height()); - - if (longitudinal_control && sm.alive("radarState")) { - update_leads(radar_state, model.getPosition()); - const auto &lead_two = radar_state.getLeadTwo(); - if (lead_one.getStatus()) { - drawLead(painter, lead_one, lead_vertices[0], surface_rect); - } - if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) { - drawLead(painter, lead_two, lead_vertices[1], surface_rect); - } - } - - painter.restore(); -} - -void ModelRenderer::update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line) { - for (int i = 0; i < 2; ++i) { - const auto &lead_data = (i == 0) ? radar_state.getLeadOne() : radar_state.getLeadTwo(); - if (lead_data.getStatus()) { - float z = line.getZ()[get_path_length_idx(line, lead_data.getDRel())]; - mapToScreen(lead_data.getDRel(), -lead_data.getYRel(), z + path_offset_z, &lead_vertices[i]); - } - } -} - -void ModelRenderer::update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) { - const auto &model_position = model.getPosition(); - float max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE); - - // update lane lines - const auto &lane_lines = model.getLaneLines(); - const auto &line_probs = model.getLaneLineProbs(); - int max_idx = get_path_length_idx(lane_lines[0], max_distance); - for (int i = 0; i < std::size(lane_line_vertices); i++) { - lane_line_probs[i] = line_probs[i]; - mapLineToPolygon(lane_lines[i], 0.025 * lane_line_probs[i], 0, &lane_line_vertices[i], max_idx); - } - - // update road edges - const auto &road_edges = model.getRoadEdges(); - const auto &edge_stds = model.getRoadEdgeStds(); - for (int i = 0; i < std::size(road_edge_vertices); i++) { - road_edge_stds[i] = edge_stds[i]; - mapLineToPolygon(road_edges[i], 0.025, 0, &road_edge_vertices[i], max_idx); - } - - // update path - if (lead.getStatus()) { - const float lead_d = lead.getDRel() * 2.; - max_distance = std::clamp((float)(lead_d - fmin(lead_d * 0.35, 10.)), 0.0f, max_distance); - } - max_idx = get_path_length_idx(model_position, max_distance); - mapLineToPolygon(model_position, 0.9, path_offset_z, &track_vertices, max_idx, false); -} - -void ModelRenderer::drawLaneLines(QPainter &painter) { - // lanelines - for (int i = 0; i < std::size(lane_line_vertices); ++i) { - painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(lane_line_probs[i], 0.0, 0.7))); - painter.drawPolygon(lane_line_vertices[i]); - } - - // road edges - for (int i = 0; i < std::size(road_edge_vertices); ++i) { - painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp(1.0 - road_edge_stds[i], 0.0, 1.0))); - painter.drawPolygon(road_edge_vertices[i]); - } -} - -void ModelRenderer::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height) { - QLinearGradient bg(0, height, 0, 0); - if (experimental_mode) { - // The first half of track_vertices are the points for the right side of the path - const auto &acceleration = model.getAcceleration().getX(); - const int max_len = std::min(track_vertices.length() / 2, acceleration.size()); - - for (int i = 0; i < max_len; ++i) { - // Some points are out of frame - int track_idx = max_len - i - 1; // flip idx to start from bottom right - if (track_vertices[track_idx].y() < 0 || track_vertices[track_idx].y() > height) continue; - - // Flip so 0 is bottom of frame - float lin_grad_point = (height - track_vertices[track_idx].y()) / height; - - // speed up: 120, slow down: 0 - float path_hue = fmax(fmin(60 + acceleration[i] * 35, 120), 0); - // FIXME: painter.drawPolygon can be slow if hue is not rounded - path_hue = int(path_hue * 100 + 0.5) / 100; - - float saturation = fmin(fabs(acceleration[i] * 1.5), 1); - float lightness = util::map_val(saturation, 0.0f, 1.0f, 0.95f, 0.62f); // lighter when grey - float alpha = util::map_val(lin_grad_point, 0.75f / 2.f, 0.75f, 0.4f, 0.0f); // matches previous alpha fade - bg.setColorAt(lin_grad_point, QColor::fromHslF(path_hue / 360., saturation, lightness, alpha)); - - // Skip a point, unless next is last - i += (i + 2) < max_len ? 1 : 0; - } - - } else { - updatePathGradient(bg); - } - - painter.setBrush(bg); - painter.drawPolygon(track_vertices); -} - -void ModelRenderer::updatePathGradient(QLinearGradient &bg) { - static const QColor throttle_colors[] = { - QColor::fromHslF(148. / 360., 0.94, 0.51, 0.4), - QColor::fromHslF(112. / 360., 1.0, 0.68, 0.35), - QColor::fromHslF(112. / 360., 1.0, 0.68, 0.0)}; - - static const QColor no_throttle_colors[] = { - QColor::fromHslF(148. / 360., 0.0, 0.95, 0.4), - QColor::fromHslF(112. / 360., 0.0, 0.95, 0.35), - QColor::fromHslF(112. / 360., 0.0, 0.95, 0.0), - }; - - // Transition speed; 0.1 corresponds to 0.5 seconds at UI_FREQ - constexpr float transition_speed = 0.1f; - - // Start transition if throttle state changes - bool allow_throttle = (*uiState()->sm)["longitudinalPlan"].getLongitudinalPlan().getAllowThrottle() || !longitudinal_control; - if (allow_throttle != prev_allow_throttle) { - prev_allow_throttle = allow_throttle; - // Invert blend factor for a smooth transition when the state changes mid-animation - blend_factor = std::max(1.0f - blend_factor, 0.0f); - } - - const QColor *begin_colors = allow_throttle ? no_throttle_colors : throttle_colors; - const QColor *end_colors = allow_throttle ? throttle_colors : no_throttle_colors; - if (blend_factor < 1.0f) { - blend_factor = std::min(blend_factor + transition_speed, 1.0f); - } - - // Set gradient colors by blending the start and end colors - bg.setColorAt(0.0f, blendColors(begin_colors[0], end_colors[0], blend_factor)); - bg.setColorAt(0.5f, blendColors(begin_colors[1], end_colors[1], blend_factor)); - bg.setColorAt(1.0f, blendColors(begin_colors[2], end_colors[2], blend_factor)); -} - -QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float t) { - if (t == 1.0f) return end; - return QColor::fromRgbF( - (1 - t) * start.redF() + t * end.redF(), - (1 - t) * start.greenF() + t * end.greenF(), - (1 - t) * start.blueF() + t * end.blueF(), - (1 - t) * start.alphaF() + t * end.alphaF()); -} - -void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, - const QPointF &vd, const QRect &surface_rect) { - const float speedBuff = 10.; - const float leadBuff = 40.; - const float d_rel = lead_data.getDRel(); - const float v_rel = lead_data.getVRel(); - - float fillAlpha = 0; - if (d_rel < leadBuff) { - fillAlpha = 255 * (1.0 - (d_rel / leadBuff)); - if (v_rel < 0) { - fillAlpha += 255 * (-1 * (v_rel / speedBuff)); - } - fillAlpha = (int)(fmin(fillAlpha, 255)); - } - - float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35; - float x = std::clamp(vd.x(), 0.f, surface_rect.width() - sz / 2); - float y = std::min(vd.y(), surface_rect.height() - sz * 0.6); - - float g_xo = sz / 5; - float g_yo = sz / 10; - - QPointF glow[] = {{x + (sz * 1.35) + g_xo, y + sz + g_yo}, {x, y - g_yo}, {x - (sz * 1.35) - g_xo, y + sz + g_yo}}; - painter.setBrush(QColor(218, 202, 37, 255)); - painter.drawPolygon(glow, std::size(glow)); - - // chevron - QPointF chevron[] = {{x + (sz * 1.25), y + sz}, {x, y}, {x - (sz * 1.25), y + sz}}; - painter.setBrush(QColor(201, 34, 49, fillAlpha)); - painter.drawPolygon(chevron, std::size(chevron)); -} - -// Projects a point in car to space to the corresponding point in full frame image space. -bool ModelRenderer::mapToScreen(float in_x, float in_y, float in_z, QPointF *out) { - Eigen::Vector3f input(in_x, in_y, in_z); - auto pt = car_space_transform * input; - *out = QPointF(pt.x() / pt.z(), pt.y() / pt.z()); - return clip_region.contains(*out); -} - -void ModelRenderer::mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off, - QPolygonF *pvd, int max_idx, bool allow_invert) { - const auto line_x = line.getX(), line_y = line.getY(), line_z = line.getZ(); - QPointF left, right; - pvd->clear(); - for (int i = 0; i <= max_idx; i++) { - // highly negative x positions are drawn above the frame and cause flickering, clip to zy plane of camera - if (line_x[i] < 0) continue; - - bool l = mapToScreen(line_x[i], line_y[i] - y_off, line_z[i] + z_off, &left); - bool r = mapToScreen(line_x[i], line_y[i] + y_off, line_z[i] + z_off, &right); - if (l && r) { - // For wider lines the drawn polygon will "invert" when going over a hill and cause artifacts - if (!allow_invert && pvd->size() && left.y() > pvd->back().y()) { - continue; - } - pvd->push_back(left); - pvd->push_front(right); - } - } -} diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h deleted file mode 100644 index 0a58d345c4..0000000000 --- a/selfdrive/ui/qt/onroad/model.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include -#include - -#ifdef SUNNYPILOT -#include "selfdrive/ui/sunnypilot/ui.h" -#else -#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); - -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); - 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); - void updatePathGradient(QLinearGradient &bg); - QColor blendColors(const QColor &start, const QColor &end, float t); - - bool longitudinal_control = false; - bool experimental_mode = false; - float blend_factor = 1.0f; - bool prev_allow_throttle = true; - float lane_line_probs[4] = {}; - float road_edge_stds[2] = {}; - float path_offset_z = 1.22f; - QPolygonF track_vertices; - QPolygonF lane_line_vertices[4] = {}; - QPolygonF road_edge_vertices[2] = {}; - QPointF lead_vertices[2] = {}; - Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero(); - QRectF clip_region; -}; diff --git a/selfdrive/ui/qt/onroad/onroad_home.cc b/selfdrive/ui/qt/onroad/onroad_home.cc deleted file mode 100644 index 7db9a05a0c..0000000000 --- a/selfdrive/ui/qt/onroad/onroad_home.cc +++ /dev/null @@ -1,69 +0,0 @@ -#include "selfdrive/ui/qt/onroad/onroad_home.h" - -#include -#include - -#include "selfdrive/ui/qt/util.h" - -OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setMargin(UI_BORDER_SIZE); - QStackedLayout *stacked_layout = new QStackedLayout; - stacked_layout->setStackingMode(QStackedLayout::StackAll); - main_layout->addLayout(stacked_layout); - - nvg = new AnnotatedCameraWidget(VISION_STREAM_ROAD, this); - - QWidget * split_wrapper = new QWidget; - split = new QHBoxLayout(split_wrapper); - split->setContentsMargins(0, 0, 0, 0); - split->setSpacing(0); - split->addWidget(nvg); - - if (getenv("DUAL_CAMERA_VIEW")) { - CameraWidget *arCam = new CameraWidget("camerad", VISION_STREAM_ROAD, this); - split->insertWidget(0, arCam); - } - - stacked_layout->addWidget(split_wrapper); - - alerts = new OnroadAlerts(this); - alerts->setAttribute(Qt::WA_TransparentForMouseEvents, true); - stacked_layout->addWidget(alerts); - - // setup stacking order - 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) { - if (!s.scene.started) { - return; - } - - alerts->updateState(s); - nvg->updateState(s); - - QColor bgColor = bg_colors[s.status]; - if (bg != bgColor) { - // repaint border - bg = bgColor; - update(); - } -} - -void OnroadWindow::offroadTransition(bool offroad) { - alerts->clear(); -} - -void OnroadWindow::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255)); -} diff --git a/selfdrive/ui/qt/onroad/onroad_home.h b/selfdrive/ui/qt/onroad/onroad_home.h deleted file mode 100644 index ad0603c095..0000000000 --- a/selfdrive/ui/qt/onroad/onroad_home.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "selfdrive/ui/qt/onroad/alerts.h" - -#ifdef SUNNYPILOT -#include "selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h" -#include "selfdrive/ui/sunnypilot/qt/onroad/alerts.h" -#define UIState UIStateSP -#define AnnotatedCameraWidget AnnotatedCameraWidgetSP -#define OnroadAlerts OnroadAlertsSP -#else -#include "selfdrive/ui/qt/onroad/annotated_camera.h" -#endif - -class OnroadWindow : public QWidget { - Q_OBJECT - -public: - OnroadWindow(QWidget* parent = 0); - -protected: - 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); -}; diff --git a/selfdrive/ui/qt/prime_state.cc b/selfdrive/ui/qt/prime_state.cc deleted file mode 100644 index f12daf1e3c..0000000000 --- a/selfdrive/ui/qt/prime_state.cc +++ /dev/null @@ -1,48 +0,0 @@ -#include "selfdrive/ui/qt/prime_state.h" - -#include - -#include "selfdrive/ui/qt/api.h" -#include "selfdrive/ui/qt/request_repeater.h" -#include "selfdrive/ui/qt/util.h" - -PrimeState::PrimeState(QObject* parent) : QObject(parent) { - const char *env_prime_type = std::getenv("PRIME_TYPE"); - auto type = env_prime_type ? env_prime_type : Params().get("PrimeType"); - - if (!type.empty()) { - prime_type = static_cast(std::atoi(type.c_str())); - } - - if (auto dongleId = getDongleId()) { - QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/"; - RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_Device", 5); - QObject::connect(repeater, &RequestRepeater::requestDone, this, &PrimeState::handleReply); - } - - // Emit the initial state change - QTimer::singleShot(1, [this]() { emit changed(prime_type); }); -} - -void PrimeState::handleReply(const QString& response, bool success) { - if (!success) return; - - QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); - if (doc.isNull()) { - qDebug() << "JSON Parse failed on getting pairing and PrimeState status"; - return; - } - - QJsonObject json = doc.object(); - bool is_paired = json["is_paired"].toBool(); - auto type = static_cast(json["prime_type"].toInt()); - setType(is_paired ? type : PrimeState::PRIME_TYPE_UNPAIRED); -} - -void PrimeState::setType(PrimeState::Type type) { - if (type != prime_type) { - prime_type = type; - Params().put("PrimeType", std::to_string(prime_type)); - emit changed(prime_type); - } -} diff --git a/selfdrive/ui/qt/prime_state.h b/selfdrive/ui/qt/prime_state.h deleted file mode 100644 index 0e2e3bb043..0000000000 --- a/selfdrive/ui/qt/prime_state.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -class PrimeState : public QObject { - Q_OBJECT - -public: - - enum Type { - PRIME_TYPE_UNKNOWN = -2, - PRIME_TYPE_UNPAIRED = -1, - PRIME_TYPE_NONE = 0, - PRIME_TYPE_MAGENTA = 1, - PRIME_TYPE_LITE = 2, - PRIME_TYPE_BLUE = 3, - PRIME_TYPE_MAGENTA_NEW = 4, - PRIME_TYPE_PURPLE = 5, - }; - - PrimeState(QObject *parent); - void setType(PrimeState::Type type); - inline PrimeState::Type currentType() const { return prime_type; } - inline bool isSubscribed() const { return prime_type > PrimeState::PRIME_TYPE_NONE; } - -signals: - void changed(PrimeState::Type prime_type); - -private: - void handleReply(const QString &response, bool success); - - PrimeState::Type prime_type = PrimeState::PRIME_TYPE_UNKNOWN; -}; diff --git a/selfdrive/ui/qt/qt_window.cc b/selfdrive/ui/qt/qt_window.cc deleted file mode 100644 index 8d3d7cf72e..0000000000 --- a/selfdrive/ui/qt/qt_window.cc +++ /dev/null @@ -1,36 +0,0 @@ -#include "selfdrive/ui/qt/qt_window.h" - -void setMainWindow(QWidget *w) { - const float scale = util::getenv("SCALE", 1.0f); - const QSize sz = QGuiApplication::primaryScreen()->size(); - - if (Hardware::PC() && scale == 1.0 && !(sz - DEVICE_SCREEN_SIZE).isValid()) { - w->setMinimumSize(QSize(640, 480)); // allow resize smaller than fullscreen - w->setMaximumSize(DEVICE_SCREEN_SIZE); - w->resize(sz); - } else { - w->setFixedSize(DEVICE_SCREEN_SIZE * scale); - } - w->show(); - -#ifdef QCOM2 - QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); - wl_surface *s = reinterpret_cast(native->nativeResourceForWindow("surface", w->windowHandle())); - wl_surface_set_buffer_transform(s, WL_OUTPUT_TRANSFORM_270); - wl_surface_commit(s); - - w->setWindowState(Qt::WindowFullScreen); - w->setVisible(true); - - // ensure we have a valid eglDisplay, otherwise the ui will silently fail - void *egl = native->nativeResourceForWindow("egldisplay", w->windowHandle()); - assert(egl != nullptr); -#endif -} - - -extern "C" { - void set_main_window(void *w) { - setMainWindow((QWidget*)w); - } -} diff --git a/selfdrive/ui/qt/qt_window.h b/selfdrive/ui/qt/qt_window.h deleted file mode 100644 index 6f16e00957..0000000000 --- a/selfdrive/ui/qt/qt_window.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -#ifdef QCOM2 -#include -#include -#include -#endif - -#include "system/hardware/hw.h" - -const QString ASSET_PATH = ":/"; -const QSize DEVICE_SCREEN_SIZE = {2160, 1080}; - -void setMainWindow(QWidget *w); diff --git a/selfdrive/ui/qt/request_repeater.cc b/selfdrive/ui/qt/request_repeater.cc deleted file mode 100644 index 7aa731898c..0000000000 --- a/selfdrive/ui/qt/request_repeater.cc +++ /dev/null @@ -1,27 +0,0 @@ -#include "selfdrive/ui/qt/request_repeater.h" - -RequestRepeater::RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey, - int period, bool while_onroad) : HttpRequest(parent) { - timer = new QTimer(this); - timer->setTimerType(Qt::VeryCoarseTimer); - QObject::connect(timer, &QTimer::timeout, [=]() { - if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) { - sendRequest(requestURL); - } - }); - - 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); }); - } - QObject::connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) { - if (success && resp != prevResp) { - params.put(cacheKey.toStdString(), resp.toStdString()); - prevResp = resp; - } - }); - } -} diff --git a/selfdrive/ui/qt/request_repeater.h b/selfdrive/ui/qt/request_repeater.h deleted file mode 100644 index a0e8bde0eb..0000000000 --- a/selfdrive/ui/qt/request_repeater.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#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: - RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool while_onroad=false); - -private: - Params params; - QTimer *timer; - QString prevResp; -}; diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc deleted file mode 100644 index 72400bd945..0000000000 --- a/selfdrive/ui/qt/sidebar.cc +++ /dev/null @@ -1,171 +0,0 @@ -#include "selfdrive/ui/qt/sidebar.h" - -#include - -#include "selfdrive/ui/qt/util.h" - -void Sidebar::drawMetric(QPainter &p, const QPair &label, QColor c, int y) { - const QRect rect = {30, y, 240, 126}; - - p.setPen(Qt::NoPen); - p.setBrush(QBrush(c)); - p.setClipRect(rect.x() + 4, rect.y(), 18, rect.height(), Qt::ClipOperation::ReplaceClip); - p.drawRoundedRect(QRect(rect.x() + 4, rect.y() + 4, 100, 118), 18, 18); - p.setClipping(false); - - QPen pen = QPen(QColor(0xff, 0xff, 0xff, 0x55)); - pen.setWidth(2); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawRoundedRect(rect, 20, 20); - - p.setPen(QColor(0xff, 0xff, 0xff)); - p.setFont(InterFont(35, QFont::DemiBold)); - p.drawText(rect.adjusted(22, 0, 0, 0), Qt::AlignCenter, label.first + "\n" + label.second); -} - -Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false), mic_indicator_pressed(false) { - home_img = loadPixmap("../assets/images/button_home.png", home_btn.size()); - flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size()); - settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio); - mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30)); - link_img = loadPixmap("../assets/icons/link.png", QSize(60, 60)); - - connect(this, &Sidebar::valueChanged, [=] { update(); }); - - setAttribute(Qt::WA_OpaquePaintEvent); - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - setFixedWidth(300); - - QObject::connect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); - - pm = std::make_unique(std::vector{"bookmarkButton"}); -} - -void Sidebar::mousePressEvent(QMouseEvent *event) { - if (onroad && home_btn.contains(event->pos())) { - flag_pressed = true; - update(); - } else if (settings_btn.contains(event->pos())) { - settings_pressed = true; - update(); - } else if (recording_audio && mic_indicator_btn.contains(event->pos())) { - mic_indicator_pressed = true; - update(); - } -} - -void Sidebar::mouseReleaseEvent(QMouseEvent *event) { - if (flag_pressed || settings_pressed || mic_indicator_pressed) { - flag_pressed = settings_pressed = mic_indicator_pressed = false; - update(); - } - if (onroad && home_btn.contains(event->pos())) { - MessageBuilder msg; - msg.initEvent().initBookmarkButton(); - pm->send("bookmarkButton", msg); - } else if (settings_btn.contains(event->pos())) { - emit openSettings(); - } else if (recording_audio && mic_indicator_btn.contains(event->pos())) { - emit openSettings(2, "RecordAudio"); - } -} - -void Sidebar::offroadTransition(bool offroad) { - onroad = !offroad; - update(); -} - -void Sidebar::updateState(const UIState &s) { - if (!isVisible()) return; - - auto &sm = *(s.sm); - - networking = networking ? networking : window()->findChild(""); - bool tethering_on = networking && networking->wifi->tethering_on; - auto deviceState = sm["deviceState"].getDeviceState(); - setProperty("netType", tethering_on ? "Hotspot": network_type[deviceState.getNetworkType()]); - int strength = tethering_on ? 4 : (int)deviceState.getNetworkStrength(); - setProperty("netStrength", strength > 0 ? strength + 1 : 0); - - ItemStatus connectStatus; - auto last_ping = deviceState.getLastAthenaPingTime(); - if (last_ping == 0) { - connectStatus = ItemStatus{{tr("CONNECT"), tr("OFFLINE")}, warning_color}; - } else { - connectStatus = nanos_since_boot() - last_ping < 80e9 - ? ItemStatus{{tr("CONNECT"), tr("ONLINE")}, good_color} - : ItemStatus{{tr("CONNECT"), tr("ERROR")}, danger_color}; - } - setProperty("connectStatus", QVariant::fromValue(connectStatus)); - - ItemStatus tempStatus = {{tr("TEMP"), tr("HIGH")}, danger_color}; - auto ts = deviceState.getThermalStatus(); - if (ts == cereal::DeviceState::ThermalStatus::GREEN) { - tempStatus = {{tr("TEMP"), tr("GOOD")}, good_color}; - } else if (ts == cereal::DeviceState::ThermalStatus::YELLOW) { - tempStatus = {{tr("TEMP"), tr("OK")}, warning_color}; - } - setProperty("tempStatus", QVariant::fromValue(tempStatus)); - - ItemStatus pandaStatus = {{tr("VEHICLE"), tr("ONLINE")}, good_color}; - if (s.scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { - pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; - } - setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); - - setProperty("recordingAudio", s.scene.recording_audio); -} - -void Sidebar::paintEvent(QPaintEvent *event) { - QPainter p(this); - drawSidebar(p); -} - -void Sidebar::drawSidebar(QPainter &p) { - p.setPen(Qt::NoPen); - p.setRenderHint(QPainter::Antialiasing); - - p.fillRect(rect(), QColor(57, 57, 57)); - - // buttons - p.setOpacity(settings_pressed ? 0.65 : 1.0); - p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img); - p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0); - p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img); - if (recording_audio) { - p.setBrush(danger_color); - p.setOpacity(mic_indicator_pressed ? 0.65 : 1.0); - p.drawRoundedRect(mic_indicator_btn, mic_indicator_btn.height() / 2, mic_indicator_btn.height() / 2); - int icon_x = mic_indicator_btn.x() + (mic_indicator_btn.width() - mic_img.width()) / 2; - int icon_y = mic_indicator_btn.y() + (mic_indicator_btn.height() - mic_img.height()) / 2; - p.drawPixmap(icon_x, icon_y, mic_img); - } - p.setOpacity(1.0); - - // network - int x = 58; - const QColor gray(0x54, 0x54, 0x54); - for (int i = 0; i < 5; ++i) { - p.setBrush(i < net_strength ? Qt::white : gray); - p.drawEllipse(x, 196, 27, 27); - x += 37; - } - - p.setFont(InterFont(35)); - p.setPen(QColor(0xff, 0xff, 0xff)); - const QRect r = QRect(58, 247, width() - 100, 50); - - if (net_type == "Hotspot") { - p.drawPixmap(r.x(), r.y() + (r.height() - link_img.height()) / 2, link_img); - } else { - p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type); - } - -#ifndef SUNNYPILOT - // metrics - 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 deleted file mode 100644 index 30a92c972e..0000000000 --- a/selfdrive/ui/qt/sidebar.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include - -#include -#include - -#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); - -class Sidebar : public QFrame { - Q_OBJECT - Q_PROPERTY(ItemStatus connectStatus MEMBER connect_status NOTIFY valueChanged); - Q_PROPERTY(ItemStatus pandaStatus MEMBER panda_status NOTIFY valueChanged); - Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged); - Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged); - Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged); - Q_PROPERTY(bool recordingAudio MEMBER recording_audio NOTIFY valueChanged); - -public: - explicit Sidebar(QWidget* parent = 0); - -signals: - void openSettings(int index = 0, const QString ¶m = ""); - void valueChanged(); - -public slots: - void offroadTransition(bool offroad); - void updateState(const UIState &s); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void drawMetric(QPainter &p, const QPair &label, QColor c, int y); - virtual void drawSidebar(QPainter &p); - - QPixmap home_img, flag_img, settings_img, mic_img, link_img; - bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed; - const QMap network_type = { - {cereal::DeviceState::NetworkType::NONE, tr("--")}, - {cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")}, - {cereal::DeviceState::NetworkType::ETHERNET, tr("ETH")}, - {cereal::DeviceState::NetworkType::CELL2_G, tr("2G")}, - {cereal::DeviceState::NetworkType::CELL3_G, tr("3G")}, - {cereal::DeviceState::NetworkType::CELL4_G, tr("LTE")}, - {cereal::DeviceState::NetworkType::CELL5_G, tr("5G")} - }; - - const QRect home_btn = QRect(60, 860, 180, 180); - const QRect settings_btn = QRect(50, 35, 200, 117); - const QRect mic_indicator_btn = QRect(158, 252, 75, 40); - const QColor good_color = QColor(255, 255, 255); - const QColor warning_color = QColor(218, 202, 37); - const QColor danger_color = QColor(201, 34, 49); - - ItemStatus connect_status, panda_status, temp_status; - QString net_type; - int net_strength = 0; - -private: - std::unique_ptr pm; - Networking *networking = nullptr; -}; diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc deleted file mode 100644 index 9af76e4aea..0000000000 --- a/selfdrive/ui/qt/util.cc +++ /dev/null @@ -1,228 +0,0 @@ -#include "selfdrive/ui/qt/util.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "common/swaglog.h" -#include "common/util.h" -#include "system/hardware/hw.h" - -QString getVersion() { - static QString version = QString::fromStdString(Params().get("Version")); - return version; -} - -QString getBrand() { - return QObject::tr("sunnypilot"); -} - -QString getUserAgent() { - return "openpilot-" + getVersion(); -} - -std::optional getDongleId() { - std::string id = Params().get("DongleId"); - - if (!id.empty() && (id != "UnregisteredDevice")) { - return QString::fromStdString(id); - } else { - return {}; - } -} - -QMap getSupportedLanguages() { - QFile f(":/languages.json"); - f.open(QIODevice::ReadOnly | QIODevice::Text); - QString val = f.readAll(); - - QJsonObject obj = QJsonDocument::fromJson(val.toUtf8()).object(); - QMap map; - for (auto key : obj.keys()) { - map[key] = obj[key].toString(); - } - return map; -} - -QString timeAgo(const QDateTime &date) { - if (!util::system_time_valid()) { - return date.date().toString(); - } - - int diff = date.secsTo(QDateTime::currentDateTimeUtc()); - - QString s; - if (diff < 60) { - s = QObject::tr("now"); - } else if (diff < 60 * 60) { - int minutes = diff / 60; - s = QObject::tr("%n minute(s) ago", "", minutes); - } else if (diff < 60 * 60 * 24) { - int hours = diff / (60 * 60); - s = QObject::tr("%n hour(s) ago", "", hours); - } else if (diff < 3600 * 24 * 7) { - int days = diff / (60 * 60 * 24); - s = QObject::tr("%n day(s) ago", "", days); - } else { - s = date.date().toString(); - } - - return s; -} - -void setQtSurfaceFormat() { - QSurfaceFormat fmt; -#ifdef __APPLE__ - fmt.setVersion(3, 2); - fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - fmt.setRenderableType(QSurfaceFormat::OpenGL); -#else - fmt.setRenderableType(QSurfaceFormat::OpenGLES); -#endif - fmt.setSamples(16); - fmt.setStencilBufferSize(1); - QSurfaceFormat::setDefaultFormat(fmt); -} - -void sigTermHandler(int s) { - std::signal(s, SIG_DFL); - qApp->quit(); -} - -void initApp(int argc, char *argv[], bool disable_hidpi) { - Hardware::set_display_power(true); - Hardware::set_brightness(65); - - // setup signal handlers to exit gracefully - std::signal(SIGINT, sigTermHandler); - std::signal(SIGTERM, sigTermHandler); - - QString app_dir; -#ifdef __APPLE__ - // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering - QApplication tmp(argc, argv); - app_dir = QCoreApplication::applicationDirPath(); - if (disable_hidpi) { - qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); - } -#else - app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path(); -#endif - - qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); - // ensure the current dir matches the exectuable's directory - QDir::setCurrent(app_dir); - - setQtSurfaceFormat(); -} - -void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - static std::map levels = { - {QtMsgType::QtDebugMsg, CLOUDLOG_DEBUG}, - {QtMsgType::QtInfoMsg, CLOUDLOG_INFO}, - {QtMsgType::QtWarningMsg, CLOUDLOG_WARNING}, - {QtMsgType::QtCriticalMsg, CLOUDLOG_ERROR}, - {QtMsgType::QtSystemMsg, CLOUDLOG_ERROR}, - {QtMsgType::QtFatalMsg, CLOUDLOG_CRITICAL}, - }; - - std::string file, function; - if (context.file != nullptr) file = context.file; - if (context.function != nullptr) function = context.function; - - auto bts = msg.toUtf8(); - cloudlog_e(levels[type], file.c_str(), context.line, function.c_str(), "%s", bts.constData()); -} - - -QWidget* topWidget(QWidget* widget) { - while (widget->parentWidget() != nullptr) widget=widget->parentWidget(); - return widget; -} - -QPixmap loadPixmap(const QString &fileName, const QSize &size, Qt::AspectRatioMode aspectRatioMode) { - if (size.isEmpty()) { - return QPixmap(fileName); - } else { - return QPixmap(fileName).scaled(size, aspectRatioMode, Qt::SmoothTransformation); - } -} - -static QHash load_bootstrap_icons() { - QHash icons; - - QFile f(":/bootstrap-icons.svg"); - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - QDomDocument xml; - xml.setContent(&f); - QDomNode n = xml.documentElement().firstChild(); - while (!n.isNull()) { - QDomElement e = n.toElement(); - if (!e.isNull() && e.hasAttribute("id")) { - QString svg_str; - QTextStream stream(&svg_str); - n.save(stream, 0); - svg_str.replace("", ""); - icons[e.attribute("id")] = svg_str.toUtf8(); - } - n = n.nextSibling(); - } - } - return icons; -} - -QPixmap bootstrapPixmap(const QString &id) { - static QHash icons = load_bootstrap_icons(); - - QPixmap pixmap; - if (auto it = icons.find(id); it != icons.end()) { - pixmap.loadFromData(it.value(), "svg"); - } - return pixmap; -} - -bool hasLongitudinalControl(const cereal::CarParams::Reader &car_params) { - // Using the experimental longitudinal toggle, returns whether longitudinal control - // will be active without needing a restart of openpilot - return car_params.getAlphaLongitudinalAvailable() - ? Params().getBool("AlphaLongitudinalEnabled") - : car_params.getOpenpilotLongitudinalControl(); -} - -// ParamWatcher - -ParamWatcher::ParamWatcher(QObject *parent) : QObject(parent) { - watcher = new QFileSystemWatcher(this); - QObject::connect(watcher, &QFileSystemWatcher::fileChanged, this, &ParamWatcher::fileChanged); -} - -void ParamWatcher::fileChanged(const QString &path) { - auto param_name = QFileInfo(path).fileName(); - auto param_value = QString::fromStdString(params.get(param_name.toStdString())); - - auto it = params_hash.find(param_name); - bool content_changed = (it == params_hash.end()) || (it.value() != param_value); - params_hash[param_name] = param_value; - // emit signal when the content changes. - if (content_changed) { - emit paramChanged(param_name, param_value); - } -} - -void ParamWatcher::addParam(const QString ¶m_name) { - watcher->addPath(QString::fromStdString(params.getParamPath(param_name.toStdString()))); -} diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h deleted file mode 100644 index 2bf1a70a62..0000000000 --- a/selfdrive/ui/qt/util.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "cereal/gen/cpp/car.capnp.h" -#include "common/params.h" - -QString getVersion(); -QString getBrand(); -QString getUserAgent(); -std::optional getDongleId(); -QMap getSupportedLanguages(); -void setQtSurfaceFormat(); -void sigTermHandler(int s); -QString timeAgo(const QDateTime &date); -void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); -void initApp(int argc, char *argv[], bool disable_hidpi = true); -QWidget* topWidget(QWidget* widget); -QPixmap loadPixmap(const QString &fileName, const QSize &size = {}, Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio); -QPixmap bootstrapPixmap(const QString &id); -bool hasLongitudinalControl(const cereal::CarParams::Reader &car_params); - -struct InterFont : public QFont { - InterFont(int pixel_size, QFont::Weight weight = QFont::Normal) : QFont("Inter") { - setPixelSize(pixel_size); - setWeight(weight); - } -}; - -class ParamWatcher : public QObject { - Q_OBJECT - -public: - ParamWatcher(QObject *parent); - void addParam(const QString ¶m_name); - -signals: - void paramChanged(const QString ¶m_name, const QString ¶m_value); - -private: - void fileChanged(const QString &path); - - QFileSystemWatcher *watcher; - QHash params_hash; - Params params; -}; diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc deleted file mode 100644 index 81ef613393..0000000000 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ /dev/null @@ -1,365 +0,0 @@ -#include "selfdrive/ui/qt/widgets/cameraview.h" - -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include -#include - -namespace { - -const char frame_vertex_shader[] = -#ifdef __APPLE__ - "#version 330 core\n" -#else - "#version 300 es\n" -#endif - "layout(location = 0) in vec4 aPosition;\n" - "layout(location = 1) in vec2 aTexCoord;\n" - "uniform mat4 uTransform;\n" - "out vec2 vTexCoord;\n" - "void main() {\n" - " gl_Position = uTransform * aPosition;\n" - " vTexCoord = aTexCoord;\n" - "}\n"; - -const char frame_fragment_shader[] = -#ifdef QCOM2 - "#version 300 es\n" - "#extension GL_OES_EGL_image_external_essl3 : enable\n" - "precision mediump float;\n" - "uniform samplerExternalOES uTexture;\n" - "in vec2 vTexCoord;\n" - "out vec4 colorOut;\n" - "void main() {\n" - " colorOut = texture(uTexture, vTexCoord);\n" - // gamma to improve worst case visibility when dark - " colorOut.rgb = pow(colorOut.rgb, vec3(1.0/1.28));\n" - "}\n"; -#else -#ifdef __APPLE__ - "#version 330 core\n" -#else - "#version 300 es\n" - "precision mediump float;\n" -#endif - "uniform sampler2D uTextureY;\n" - "uniform sampler2D uTextureUV;\n" - "in vec2 vTexCoord;\n" - "out vec4 colorOut;\n" - "void main() {\n" - " float y = texture(uTextureY, vTexCoord).r;\n" - " vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n" - " float r = y + 1.402 * uv.y;\n" - " float g = y - 0.344 * uv.x - 0.714 * uv.y;\n" - " float b = y + 1.772 * uv.x;\n" - " colorOut = vec4(r, g, b, 1.0);\n" - "}\n"; -#endif - -} // namespace - -CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : - stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) { - setAttribute(Qt::WA_OpaquePaintEvent); - qRegisterMetaType>("availableStreams"); - QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); - QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); - QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); - QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); -} - -CameraWidget::~CameraWidget() { - makeCurrent(); - stopVipcThread(); - if (isValid()) { - glDeleteVertexArrays(1, &frame_vao); - glDeleteBuffers(1, &frame_vbo); - glDeleteBuffers(1, &frame_ibo); -#ifndef QCOM2 - glDeleteTextures(2, textures); -#endif - } - doneCurrent(); -} - -// Qt uses device-independent pixels, depending on platform this may be -// different to what OpenGL uses -int CameraWidget::glWidth() { - return width() * devicePixelRatio(); -} - -int CameraWidget::glHeight() { - return height() * devicePixelRatio(); -} - -void CameraWidget::initializeGL() { - initializeOpenGLFunctions(); - - program = std::make_unique(context()); - bool ret = program->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader); - assert(ret); - ret = program->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader); - assert(ret); - - program->link(); - GLint frame_pos_loc = program->attributeLocation("aPosition"); - GLint frame_texcoord_loc = program->attributeLocation("aTexCoord"); - - auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f); - const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3}; - const float frame_coords[4][4] = { - {-1.0, -1.0, x2, y1}, // bl - {-1.0, 1.0, x2, y2}, // tl - { 1.0, 1.0, x1, y2}, // tr - { 1.0, -1.0, x1, y1}, // br - }; - - glGenVertexArrays(1, &frame_vao); - glBindVertexArray(frame_vao); - glGenBuffers(1, &frame_vbo); - glBindBuffer(GL_ARRAY_BUFFER, frame_vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW); - glEnableVertexAttribArray(frame_pos_loc); - glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE, - sizeof(frame_coords[0]), (const void *)0); - glEnableVertexAttribArray(frame_texcoord_loc); - glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE, - sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2)); - glGenBuffers(1, &frame_ibo); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); - - glUseProgram(program->programId()); - -#ifdef QCOM2 - glUniform1i(program->uniformLocation("uTexture"), 0); -#else - glGenTextures(2, textures); - glUniform1i(program->uniformLocation("uTextureY"), 0); - glUniform1i(program->uniformLocation("uTextureUV"), 1); -#endif -} - -void CameraWidget::showEvent(QShowEvent *event) { - if (!vipc_thread) { - clearFrames(); - vipc_thread = new QThread(); - connect(vipc_thread, &QThread::started, [=]() { vipcThread(); }); - connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater); - vipc_thread->start(); - } -} - -void CameraWidget::stopVipcThread() { - makeCurrent(); - if (vipc_thread) { - vipc_thread->requestInterruption(); - vipc_thread->quit(); - vipc_thread->wait(); - vipc_thread = nullptr; - } - -#ifdef QCOM2 - EGLDisplay egl_display = eglGetCurrentDisplay(); - assert(egl_display != EGL_NO_DISPLAY); - for (auto &pair : egl_images) { - eglDestroyImageKHR(egl_display, pair.second); - assert(eglGetError() == EGL_SUCCESS); - } - egl_images.clear(); -#endif -} - -void CameraWidget::availableStreamsUpdated(std::set streams) { - available_streams = streams; -} - -mat4 CameraWidget::calcFrameMatrix() { - // Scale the frame to fit the widget while maintaining the aspect ratio. - float widget_aspect_ratio = (float)width() / height(); - float frame_aspect_ratio = (float)stream_width / stream_height; - float zx = std::min(frame_aspect_ratio / widget_aspect_ratio, 1.0f); - float zy = std::min(widget_aspect_ratio / frame_aspect_ratio, 1.0f); - - return mat4{{ - zx, 0.0, 0.0, 0.0, - 0.0, zy, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - }}; -} - -void CameraWidget::paintGL() { - glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); - glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); - - std::lock_guard lk(frame_lock); - if (frames.empty()) return; - - int frame_idx = frames.size() - 1; - - // Always draw latest frame until sync logic is more stable - // for (frame_idx = 0; frame_idx < frames.size() - 1; frame_idx++) { - // if (frames[frame_idx].first == draw_frame_id) break; - // } - - // Log duplicate/dropped frames - if (frames[frame_idx].first == prev_frame_id) { - qDebug() << "Drawing same frame twice" << frames[frame_idx].first; - } else if (frames[frame_idx].first != prev_frame_id + 1) { - qDebug() << "Skipped frame" << frames[frame_idx].first; - } - prev_frame_id = frames[frame_idx].first; - VisionBuf *frame = frames[frame_idx].second; - assert(frame != nullptr); - - auto frame_mat = calcFrameMatrix(); - - glViewport(0, 0, glWidth(), glHeight()); - glBindVertexArray(frame_vao); - glUseProgram(program->programId()); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - -#ifdef QCOM2 - // no frame copy - glActiveTexture(GL_TEXTURE0); - glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, egl_images[frame->idx]); - assert(glGetError() == GL_NO_ERROR); -#else - // fallback to copy - glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, textures[0]); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, frame->y); - assert(glGetError() == GL_NO_ERROR); - - glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); - glActiveTexture(GL_TEXTURE0 + 1); - glBindTexture(GL_TEXTURE_2D, textures[1]); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, frame->uv); - assert(glGetError() == GL_NO_ERROR); -#endif - - glUniformMatrix4fv(program->uniformLocation("uTransform"), 1, GL_TRUE, frame_mat.v); - glEnableVertexAttribArray(0); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (const void *)0); - glDisableVertexAttribArray(0); - glBindVertexArray(0); - glBindTexture(GL_TEXTURE_2D, 0); - glActiveTexture(GL_TEXTURE0); - glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); -} - -void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) { - makeCurrent(); - stream_width = vipc_client->buffers[0].width; - stream_height = vipc_client->buffers[0].height; - stream_stride = vipc_client->buffers[0].stride; - -#ifdef QCOM2 - EGLDisplay egl_display = eglGetCurrentDisplay(); - assert(egl_display != EGL_NO_DISPLAY); - for (auto &pair : egl_images) { - eglDestroyImageKHR(egl_display, pair.second); - } - egl_images.clear(); - - for (int i = 0; i < vipc_client->num_buffers; i++) { // import buffers into OpenGL - int fd = dup(vipc_client->buffers[i].fd); // eglDestroyImageKHR will close, so duplicate - EGLint img_attrs[] = { - EGL_WIDTH, (int)vipc_client->buffers[i].width, - EGL_HEIGHT, (int)vipc_client->buffers[i].height, - EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_NV12, - EGL_DMA_BUF_PLANE0_FD_EXT, fd, - EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, - EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)vipc_client->buffers[i].stride, - EGL_DMA_BUF_PLANE1_FD_EXT, fd, - EGL_DMA_BUF_PLANE1_OFFSET_EXT, (int)vipc_client->buffers[i].uv_offset, - EGL_DMA_BUF_PLANE1_PITCH_EXT, (int)vipc_client->buffers[i].stride, - EGL_NONE - }; - egl_images[i] = eglCreateImageKHR(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, img_attrs); - assert(eglGetError() == EGL_SUCCESS); - } -#else - glBindTexture(GL_TEXTURE_2D, textures[0]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); - assert(glGetError() == GL_NO_ERROR); - - glBindTexture(GL_TEXTURE_2D, textures[1]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); - assert(glGetError() == GL_NO_ERROR); -#endif -} - -void CameraWidget::vipcFrameReceived() { - update(); -} - -void CameraWidget::vipcThread() { - VisionStreamType cur_stream = requested_stream_type; - std::unique_ptr vipc_client; - VisionIpcBufExtra meta_main = {0}; - - while (!QThread::currentThread()->isInterruptionRequested()) { - if (!vipc_client || cur_stream != requested_stream_type) { - clearFrames(); - qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream; - cur_stream = requested_stream_type; - vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); - } - active_stream_type = cur_stream; - - if (!vipc_client->connected) { - clearFrames(); - auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); - if (streams.empty()) { - QThread::msleep(100); - continue; - } - emit vipcAvailableStreamsUpdated(streams); - - if (!vipc_client->connect(false)) { - QThread::msleep(100); - continue; - } - emit vipcThreadConnected(vipc_client.get()); - } - - if (VisionBuf *buf = vipc_client->recv(&meta_main, 1000)) { - { - std::lock_guard lk(frame_lock); - frames.push_back(std::make_pair(meta_main.frame_id, buf)); - while (frames.size() > FRAME_BUFFER_SIZE) { - frames.pop_front(); - } - } - emit vipcThreadFrameReceived(); - } else { - if (!isVisible()) { - vipc_client->connected = false; - } - } - } -} - -void CameraWidget::clearFrames() { - std::lock_guard lk(frame_lock); - frames.clear(); - available_streams.clear(); -} diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h deleted file mode 100644 index e446ef5987..0000000000 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#ifdef QCOM2 -#define EGL_EGLEXT_PROTOTYPES -#define EGL_NO_X11 -#define GL_TEXTURE_EXTERNAL_OES 0x8D65 -#include -#include -#include -#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; - -class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions { - Q_OBJECT - -public: - using QOpenGLWidget::QOpenGLWidget; - explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); - ~CameraWidget(); - void setBackgroundColor(const QColor &color) { bg = color; } - void setFrameId(int frame_id) { draw_frame_id = frame_id; } - void setStreamType(VisionStreamType type) { requested_stream_type = type; } - VisionStreamType getStreamType() { return active_stream_type; } - void stopVipcThread(); - -signals: - void clicked(); - void vipcThreadConnected(VisionIpcClient *); - void vipcThreadFrameReceived(); - void vipcAvailableStreamsUpdated(std::set); - -protected: - void paintGL() override; - void initializeGL() override; - void showEvent(QShowEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } - virtual mat4 calcFrameMatrix(); - void vipcThread(); - void clearFrames(); - - int glWidth(); - int glHeight(); - - GLuint frame_vao, frame_vbo, frame_ibo; - GLuint textures[2]; - std::unique_ptr program; - QColor bg = QColor("#000000"); - -#ifdef QCOM2 - std::map egl_images; -#endif - - std::string stream_name; - int stream_width = 0; - int stream_height = 0; - int stream_stride = 0; - std::atomic active_stream_type; - std::atomic requested_stream_type; - std::set available_streams; - QThread *vipc_thread = nullptr; - std::recursive_mutex frame_lock; - std::deque> frames; - uint32_t draw_frame_id = 0; - uint32_t prev_frame_id = 0; - -protected slots: - void vipcConnected(VisionIpcClient *vipc_client); - void vipcFrameReceived(); - void availableStreamsUpdated(std::set streams); -}; - -Q_DECLARE_METATYPE(std::set); diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc deleted file mode 100644 index df6e6932c7..0000000000 --- a/selfdrive/ui/qt/widgets/controls.cc +++ /dev/null @@ -1,145 +0,0 @@ -#include "selfdrive/ui/qt/widgets/controls.h" - -#include -#include - -#include "selfdrive/ui/qt/util.h" - -AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) { -#ifndef SUNNYPILOT - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setMargin(0); - - hlayout = new QHBoxLayout; - hlayout->setMargin(0); - hlayout->setSpacing(20); - - // left icon - icon_label = new QLabel(this); - hlayout->addWidget(icon_label); - if (!icon.isEmpty()) { - icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); - icon_label->setPixmap(icon_pixmap); - icon_label->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); - } - icon_label->setVisible(!icon.isEmpty()); - - // title - title_label = new QPushButton(title); - title_label->setFixedHeight(120); - title_label->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; border: none;"); - hlayout->addWidget(title_label, 1); - - // value next to control button - value = new ElidedLabel(); - 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(); -#endif -} - -void AbstractControl::hideEvent(QHideEvent *e) { - if (description != nullptr) { - description->hide(); - } -} - -// controls - -ButtonControl::ButtonControl(const QString &title, const QString &text, const QString &desc, QWidget *parent) : AbstractControl(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, &ButtonControl::clicked); - hlayout->addWidget(&btn); -} - -// ElidedLabel - -ElidedLabel::ElidedLabel(QWidget *parent) : ElidedLabel({}, parent) {} - -ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - setMinimumWidth(1); -} - -void ElidedLabel::resizeEvent(QResizeEvent* event) { - QLabel::resizeEvent(event); - lastText_ = elidedText_ = ""; -} - -void ElidedLabel::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()); -} - -// ParamControl - -ParamControl::ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : ToggleControl(title, desc, icon, false, parent) { - key = param.toStdString(); - QObject::connect(this, &ParamControl::toggleFlipped, this, &ParamControl::toggleClicked); -} - -void ParamControl::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/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h deleted file mode 100644 index bf56ab185d..0000000000 --- a/selfdrive/ui/qt/widgets/controls.h +++ /dev/null @@ -1,319 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "common/params.h" -#include "selfdrive/ui/qt/widgets/input.h" -#include "selfdrive/ui/qt/widgets/toggle.h" - -class ElidedLabel : public QLabel { - Q_OBJECT - -public: - explicit ElidedLabel(QWidget *parent = 0); - explicit ElidedLabel(const QString &text, QWidget *parent = 0); - -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 AbstractControl : public QFrame { - Q_OBJECT - -public: - virtual void setDescription(const QString &desc) { - if (description) description->setText(desc); - } - - void setTitle(const QString &title) { - title_label->setText(title); - } - - void setValue(const QString &val) { - value->setText(val); - } - - virtual const QString getDescription() { - return description->text(); - } - - QLabel *icon_label; - QPixmap icon_pixmap; - -public slots: - virtual void showDescription() { - description->setVisible(true); - } - -signals: - void showDescriptionEvent(); - -protected: - AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); - void hideEvent(QHideEvent *e) override; - - QHBoxLayout *hlayout; - QPushButton *title_label; - -private: - ElidedLabel *value; - QLabel *description = nullptr; -}; - -// widget to display a value -class LabelControl : public AbstractControl { - Q_OBJECT - -public: - LabelControl(const QString &title, const QString &text = "", const QString &desc = "", QWidget *parent = nullptr) : AbstractControl(title, desc, "", parent) { - label.setText(text); - label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); - hlayout->addWidget(&label); - } - void setText(const QString &text) { label.setText(text); } - -private: - ElidedLabel label; -}; - -// widget for a button with a label -class ButtonControl : public AbstractControl { - Q_OBJECT - -public: - ButtonControl(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(); } - -signals: - void clicked(); - -public slots: - void setEnabled(bool enabled) { btn.setEnabled(enabled); } - -private: - QPushButton btn; -}; - -class ToggleControl : public AbstractControl { - Q_OBJECT - -public: - ToggleControl(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControl(title, desc, icon, parent) { - toggle.setFixedSize(150, 100); - if (state) { - toggle.togglePosition(); - } - hlayout->addWidget(&toggle); - QObject::connect(&toggle, &Toggle::stateChanged, this, &ToggleControl::toggleFlipped); - } - - void setEnabled(bool enabled) { - toggle.setEnabled(enabled); - toggle.update(); - } - -signals: - void toggleFlipped(bool state); - -protected: - Toggle toggle; -}; - -// widget to toggle params -class ParamControl : public ToggleControl { - Q_OBJECT - -public: - ParamControl(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(); - } - -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 MultiButtonControl : public AbstractControl { - Q_OBJECT -public: - MultiButtonControl(const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 225) : AbstractControl(title, desc, icon) { - const QString style = R"( - QPushButton { - border-radius: 50px; - font-size: 40px; - font-weight: 500; - height:100px; - padding: 0 25 0 25; - color: #E4E4E4; - background-color: #393939; - } - QPushButton:pressed { - background-color: #4a4a4a; - } - QPushButton:checked:enabled { - background-color: #33Ab4C; - } - QPushButton:checked:disabled { - background-color: #9933Ab4C; - } - QPushButton:disabled { - color: #33E4E4E4; - } - )"; - - 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 == 0); - button->setStyleSheet(style); - button->setMinimumWidth(minimum_button_width); - hlayout->addWidget(button); - button_group->addButton(button, i); - } - - QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), this, &MultiButtonControl::buttonClicked); - } - - void setEnabled(bool enable) { - for (auto btn : button_group->buttons()) { - btn->setEnabled(enable); - } - } - - void setCheckedButton(int id) { - button_group->button(id)->setChecked(true); - } - -signals: - void buttonClicked(int id); - -protected: - QButtonGroup *button_group; -}; - -class ButtonParamControl : public MultiButtonControl { - Q_OBJECT -public: - ButtonParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 225) : MultiButtonControl(title, desc, icon, - button_texts, minimum_button_width) { - key = param.toStdString(); - int value = atoi(params.get(key).c_str()); - - if (value > 0 && value < button_group->buttons().size()) { - button_group->button(value)->setChecked(true); - } - - QObject::connect(this, QOverload::of(&MultiButtonControl::buttonClicked), [=](int id) { - params.put(key, std::to_string(id)); - }); - } - - void refresh() { - int value = atoi(params.get(key).c_str()); - button_group->button(value)->setChecked(true); - } - - void showEvent(QShowEvent *event) override { - refresh(); - } - -private: - std::string key; - Params params; -}; - -class ListWidget : public QWidget { - Q_OBJECT - public: - explicit ListWidget(QWidget *parent = 0) : QWidget(parent), 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(1); - } - 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); } - -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()) { - QRect r = inner_layout.itemAt(i)->geometry(); - int bottom = r.bottom() + inner_layout.spacing() / 2; - p.drawLine(r.left() + 40, bottom, r.right() - 40, bottom); - } - } - } - QVBoxLayout outer_layout; - QVBoxLayout inner_layout; -}; - -// convenience class for wrapping layouts -class LayoutWidget : public QWidget { - Q_OBJECT - -public: - LayoutWidget(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) { - setLayout(l); - } -}; diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc deleted file mode 100644 index efd330587a..0000000000 --- a/selfdrive/ui/qt/widgets/input.cc +++ /dev/null @@ -1,606 +0,0 @@ -#include "selfdrive/ui/qt/widgets/input.h" - -#include -#include -#include - -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/widgets/scrollview.h" - - -DialogBase::DialogBase(QWidget *parent) : QDialog(parent) { - Q_ASSERT(parent != nullptr); - parent->installEventFilter(this); - - setStyleSheet(R"( - * { - outline: none; - color: white; - font-family: Inter; - } - DialogBase { - background-color: black; - } - QPushButton { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - color: white; - background-color: #333333; - } - QPushButton:pressed { - background-color: #444444; - } - )"); -} - -bool DialogBase::eventFilter(QObject *o, QEvent *e) { - if (o == parent() && e->type() == QEvent::Hide) { - reject(); - } - return QDialog::eventFilter(o, e); -} - -int DialogBase::exec() { - setMainWindow(this); - return QDialog::exec(); -} - -InputDialog::InputDialog(const QString &title, QWidget *parent, const QString &subtitle, bool secret) : DialogBase(parent) { - main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(50, 55, 50, 50); - main_layout->setSpacing(0); - - // build header - QHBoxLayout *header_layout = new QHBoxLayout(); - - QVBoxLayout *vlayout = new QVBoxLayout; - header_layout->addLayout(vlayout); - label = new QLabel(title, this); - label->setStyleSheet("font-size: 90px; font-weight: bold;"); - vlayout->addWidget(label, 1, Qt::AlignTop | Qt::AlignLeft); - - if (!subtitle.isEmpty()) { - sublabel = new QLabel(subtitle, this); - sublabel->setStyleSheet("font-size: 55px; font-weight: light; color: #BDBDBD;"); - vlayout->addWidget(sublabel, 1, Qt::AlignTop | Qt::AlignLeft); - } - - QPushButton* cancel_btn = new QPushButton(tr("Cancel")); - cancel_btn->setFixedSize(386, 125); - cancel_btn->setStyleSheet(R"( - QPushButton { - font-size: 48px; - border-radius: 10px; - color: #E4E4E4; - background-color: #333333; - } - QPushButton:pressed { - background-color: #444444; - } - )"); - header_layout->addWidget(cancel_btn, 0, Qt::AlignRight); - QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::reject); - QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::cancel); - - main_layout->addLayout(header_layout); - - // text box - main_layout->addStretch(2); - - QWidget *textbox_widget = new QWidget; - textbox_widget->setObjectName("textbox"); - QHBoxLayout *textbox_layout = new QHBoxLayout(textbox_widget); - textbox_layout->setContentsMargins(50, 0, 50, 0); - - textbox_widget->setStyleSheet(R"( - #textbox { - margin-left: 50px; - margin-right: 50px; - border-radius: 0; - border-bottom: 3px solid #BDBDBD; - } - * { - border: none; - font-size: 80px; - font-weight: light; - background-color: transparent; - } - )"); - - line = new QLineEdit(); - line->setStyleSheet("lineedit-password-character: 8226; lineedit-password-mask-delay: 1500;"); - textbox_layout->addWidget(line, 1); - - if (secret) { - eye_btn = new QPushButton(); - eye_btn->setCheckable(true); - eye_btn->setFixedSize(150, 120); - QObject::connect(eye_btn, &QPushButton::toggled, [=](bool checked) { - if (checked) { - eye_btn->setIcon(QIcon(ASSET_PATH + "icons/eye_closed.svg")); - eye_btn->setIconSize(QSize(81, 54)); - line->setEchoMode(QLineEdit::Password); - } else { - eye_btn->setIcon(QIcon(ASSET_PATH + "icons/eye_open.svg")); - eye_btn->setIconSize(QSize(81, 44)); - line->setEchoMode(QLineEdit::Normal); - } - }); - eye_btn->toggle(); - eye_btn->setChecked(false); - textbox_layout->addWidget(eye_btn); - } - - main_layout->addWidget(textbox_widget, 0, Qt::AlignBottom); - main_layout->addSpacing(25); - - k = new Keyboard(this); - QObject::connect(k, &Keyboard::emitEnter, this, &InputDialog::handleEnter); - QObject::connect(k, &Keyboard::emitBackspace, this, [=]() { - line->backspace(); - }); - QObject::connect(k, &Keyboard::emitKey, this, [=](const QString &key) { - line->insert(key.left(1)); - }); - - main_layout->addWidget(k, 2, Qt::AlignBottom); -} - -QString InputDialog::getText(const QString &prompt, QWidget *parent, const QString &subtitle, - bool secret, int minLength, const QString &defaultText) { - InputDialog d(prompt, parent, subtitle, secret); - d.line->setText(defaultText); - d.setMinLength(minLength); - const int ret = d.exec(); - return ret ? d.text() : QString(); -} - -QString InputDialog::text() { - return line->text(); -} - -void InputDialog::show() { - setMainWindow(this); -} - -void InputDialog::handleEnter() { - if (line->text().length() >= minLength) { - done(QDialog::Accepted); - emitText(line->text()); - } else { - setMessage(tr("Need at least %n character(s)!", "", minLength), false); - } -} - -void InputDialog::setMessage(const QString &message, bool clearInputField) { - label->setText(message); - if (clearInputField) { - line->setText(""); - } -} - -void InputDialog::setMinLength(int length) { - minLength = length; -} - -// ConfirmationDialog - -ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, const QString &cancel_text, - const bool rich, QWidget *parent) : DialogBase(parent) { - QFrame *container = new QFrame(this); - container->setStyleSheet(R"( - QFrame { background-color: #1B1B1B; color: #C9C9C9; } - #confirm_btn { background-color: #465BEA; } - #confirm_btn:pressed { background-color: #3049F4; } - )"); - QVBoxLayout *main_layout = new QVBoxLayout(container); - main_layout->setContentsMargins(32, rich ? 32 : 120, 32, 32); - - QLabel *prompt = new QLabel(prompt_text, this); - prompt->setWordWrap(true); - prompt->setAlignment(rich ? Qt::AlignLeft : Qt::AlignHCenter); - prompt->setStyleSheet((rich ? "font-size: 42px; font-weight: light;" : "font-size: 70px; font-weight: bold;") + QString(" margin: 45px;")); - main_layout->addWidget(rich ? (QWidget*)new ScrollView(prompt, this) : (QWidget*)prompt, 1, Qt::AlignTop); - - // cancel + confirm buttons - QHBoxLayout *btn_layout = new QHBoxLayout(); - btn_layout->setSpacing(30); - main_layout->addLayout(btn_layout); - - if (cancel_text.length()) { - QPushButton* cancel_btn = new QPushButton(cancel_text); - btn_layout->addWidget(cancel_btn); - QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); - } - - if (confirm_text.length()) { - QPushButton* confirm_btn = new QPushButton(confirm_text); - confirm_btn->setObjectName("confirm_btn"); - btn_layout->addWidget(confirm_btn); - QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); - } - - QVBoxLayout *outer_layout = new QVBoxLayout(this); - int margin = rich ? 100 : 200; - outer_layout->setContentsMargins(margin, margin, margin, margin); - outer_layout->addWidget(container); -} - -bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent) { - ConfirmationDialog d(prompt_text, tr("Ok"), "", false, parent); - return d.exec(); -} - -bool ConfirmationDialog::confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent) { - ConfirmationDialog d(prompt_text, confirm_text, tr("Cancel"), false, parent); - return d.exec(); -} - -bool ConfirmationDialog::rich(const QString &prompt_text, QWidget *parent) { - ConfirmationDialog d(prompt_text, tr("Ok"), "", true, parent); - return d.exec(); -} - -// MultiOptionDialog - -MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) : DialogBase(parent) { - QFrame *container = new QFrame(this); - container->setStyleSheet(R"( - QFrame { background-color: #1B1B1B; } - #confirm_btn[enabled="false"] { background-color: #2B2B2B; } - #confirm_btn:enabled { background-color: #465BEA; } - #confirm_btn:enabled:pressed { background-color: #3049F4; } - )"); - - QVBoxLayout *main_layout = new QVBoxLayout(container); - main_layout->setContentsMargins(55, 50, 55, 50); - - QLabel *title = new QLabel(prompt_text, this); - title->setStyleSheet("font-size: 70px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - main_layout->addSpacing(25); - - QWidget *listWidget = new QWidget(this); - QVBoxLayout *listLayout = new QVBoxLayout(listWidget); - listLayout->setSpacing(20); - listWidget->setStyleSheet(R"( - QPushButton { - height: 135; - padding: 0px 50px; - text-align: left; - font-size: 55px; - font-weight: 300; - border-radius: 10px; - background-color: #4F4F4F; - } - QPushButton:checked { background-color: #465BEA; } - )"); - - QButtonGroup *group = new QButtonGroup(listWidget); - group->setExclusive(true); - - QPushButton *confirm_btn = new QPushButton(tr("Select")); - confirm_btn->setObjectName("confirm_btn"); - confirm_btn->setEnabled(false); - - for (const QString &s : l) { - QPushButton *selectionLabel = new QPushButton(s); - selectionLabel->setCheckable(true); - selectionLabel->setChecked(s == current); - QObject::connect(selectionLabel, &QPushButton::toggled, [=](bool checked) { - if (checked) selection = s; - if (selection != current) { - confirm_btn->setEnabled(true); - } else { - confirm_btn->setEnabled(false); - } - }); - - group->addButton(selectionLabel); - listLayout->addWidget(selectionLabel); - } - // add stretch to keep buttons spaced correctly - listLayout->addStretch(1); - - ScrollView *scroll_view = new ScrollView(listWidget, this); - scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - - main_layout->addWidget(scroll_view); - main_layout->addSpacing(35); - - // cancel + confirm buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *cancel_btn = new QPushButton(tr("Cancel")); - QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); - QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); - blayout->addWidget(cancel_btn); - blayout->addWidget(confirm_btn); - - QVBoxLayout *outer_layout = new QVBoxLayout(this); - outer_layout->setContentsMargins(50, 50, 50, 50); - outer_layout->addWidget(container); -} - -QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) { - MultiOptionDialog d(prompt_text, l, current, parent); - if (d.exec()) { - return d.selection; - } - return ""; -} - -TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList &items, - const QString ¤t, const QString &favParam, QWidget *parent) : DialogBase(parent) { - QFrame *container = new QFrame(this); - container->setStyleSheet(R"( - QFrame { background-color: #1B1B1B; } - #confirm_btn[enabled="false"] { background-color: #2B2B2B; } - #confirm_btn:enabled { background-color: #465BEA; } - #confirm_btn:enabled:pressed { background-color: #3049F4; } - QTreeWidget { - background-color: transparent; - border: none; - } - QTreeWidget::item { - height: 135; - padding: 0px 50px; - margin: 5px; - text-align: left; - font-size: 55px; - font-weight: 300; - border-radius: 10px; - background-color: #4F4F4F; - color: white; - } - QTreeWidget::item:selected { - background-color: #465BEA; - } - QTreeWidget::branch { - background-color: transparent; - } - )"); - - QVBoxLayout *main_layout = new QVBoxLayout(container); - main_layout->setContentsMargins(55, 50, 55, 50); - - QLabel *title = new QLabel(prompt_text, this); - title->setStyleSheet("font-size: 70px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - main_layout->addSpacing(25); - - iconBlank = QIcon("../../sunnypilot/selfdrive/assets/icons/star-empty.png"); - iconFilled = QIcon ("../../sunnypilot/selfdrive/assets/icons/star-filled.png"); - - treeWidget = new QTreeWidget(this); - treeWidget->setHeaderHidden(true); - treeWidget->setIndentation(50); - treeWidget->setExpandsOnDoubleClick(false); // Disable double-click expansion - treeWidget->setAnimated(true); - treeWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - treeWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - treeWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - treeWidget->setDragEnabled(false); - treeWidget->setMouseTracking(true); - - // Connect single-click to expand/collapse - QObject::connect(treeWidget, &QTreeWidget::itemClicked, [=](QTreeWidgetItem *item, int) { - if (item->childCount() > 0) { - item->setExpanded(!item->isExpanded()); - treeWidget->scrollToItem(item->child(0), QAbstractItemView::EnsureVisible); - } - }); - - QScroller::grabGesture(treeWidget->viewport(), QScroller::LeftMouseButtonGesture); - - // Create initial list of favorites from param - const QString favs = QString::fromStdString(params.get(favParam.toStdString())); - mapFavs = new QMap>(); - favRefs = new QStringList(favs.split(";")); - for (const QString &item : *favRefs) - { - mapFavs->insert( item, {}); - } - - // Populate tree - QListIterator iter(items); - while (iter.hasNext()) { - TreeFolder currItem = iter.next(); - QString prevFolder; - QString currentFolder; - if (currItem.folder.isEmpty()) { - for (const TreeNode &item : currItem.items) { - QTreeWidgetItem *topLevel = new QTreeWidgetItem(); - topLevel->setText(0, item.displayName); - topLevel->setData(0, Qt::UserRole, item.ref); - topLevel->setFlags(topLevel->flags() | Qt::ItemIsSelectable); - treeWidget->addTopLevelItem(topLevel); - if (item.ref == current) { - topLevel->setSelected(true); - } - } - } else { - QList folders = treeWidget->findItems(currItem.folder, Qt::MatchExactly, 0); - QTreeWidgetItem *folderItem = nullptr; - if (folders.isEmpty()) { - folderItem = new QTreeWidgetItem(treeWidget); - } else { - folderItem = folders.first(); - } - folderItem->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png"))); - folderItem->setText(0, " " + currItem.folder); - folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate); - folderItem->setFlags(folderItem->flags() & ~Qt::ItemIsSelectable); - - for (const TreeNode item : currItem.items) - { - QTreeWidgetItem *childItem = addChildItem(item.displayName, item.ref, folderItem); - if (item.ref == current) { - childItem->setSelected(true); - folderItem->setExpanded(true); - } - } - } - } - - // Create favorites folder - favorites = new QTreeWidgetItem(); - favorites->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png"))); - favorites->setText(0, " " + tr("Favorites")); - favorites->setFlags(favorites->flags() | Qt::ItemIsAutoTristate); - favorites->setFlags(favorites->flags() & ~Qt::ItemIsSelectable); - treeWidget->insertTopLevelItem(1, favorites); - - // Create favorite nodes - for (int i = favRefs->size() - 1; i >= 0; --i) { - QString item = favRefs->at(i); - if (item.isEmpty()) continue; - - QTreeWidgetItemIterator treeIt(treeWidget); - QTreeWidgetItem *nodeItem = nullptr; - while (*treeIt) { - if (item == (*treeIt)->data(0, Qt::UserRole).toString()) { - nodeItem = (*treeIt); - break; - } - ++treeIt; - } - if (nodeItem == nullptr) continue; - - QTreeWidgetItem *childItem = addChildItem(nodeItem->text(0), - nodeItem->data(0, Qt::UserRole).toString(), favorites); - if (item == current) { - treeWidget->collapseAll(); - childItem->setSelected(true); - favorites->setExpanded(true); - } - } - - confirm_btn = new QPushButton(tr("Select")); - confirm_btn->setObjectName("confirm_btn"); - confirm_btn->setEnabled(false); - - QObject::connect(treeWidget, &QTreeWidget::itemSelectionChanged, [=]() { - QList selectedItems = treeWidget->selectedItems(); - if (!selectedItems.isEmpty()) { - selection = selectedItems.first()->data(0, Qt::UserRole).toString(); - confirm_btn->setEnabled(selection != current); - } - }); - - ScrollView *scroll_view = new ScrollView(treeWidget, this); - scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - - main_layout->addWidget(scroll_view); - main_layout->addSpacing(35); - - // cancel + confirm buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *cancel_btn = new QPushButton(tr("Cancel")); - QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); - QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); - blayout->addWidget(cancel_btn); - blayout->addWidget(confirm_btn); - - QVBoxLayout *outer_layout = new QVBoxLayout(this); - outer_layout->setContentsMargins(50, 50, 50, 50); - outer_layout->addWidget(container); -} - -QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList &items, - const QString ¤t, const QString &favParam, QWidget *parent) { - TreeOptionDialog d(prompt_text, items, current, favParam, parent); - if (d.exec()) { - return d.selection; - } - return ""; -} - -/** - * Handles the addition or removal of items from the "favorites" list based on the provided reference identifier. - * - * @param displayName The text label associated with the item to be added or removed in the favorites. - * @param ref A unique reference key identifying the item. - * @param btn A pointer to the QPushButton associated with the item. The button's icon is updated to indicate - * whether the item is currently favorited or not. - * - * If the item is already in the favorites, it is removed from the list, its associated buttons have their - * icons reset, and the favorites tree is updated accordingly. If the item is not in the favorites, it is - * added to the list, a new associated button is created, and the favorites tree is updated. The current - * state of the favorites is stored in the Params object as a semicolon-separated string. - */ -void TreeOptionDialog::handleFavorites(const QString &displayName, const QString &ref, QPushButton *btn) { - if (mapFavs->keys().contains(ref)) { // Remove from favorites - for (auto *itemBtn:mapFavs->value(ref)) - { - itemBtn->setIcon(iconBlank); - } - mapFavs->remove(ref); - favRefs->removeAll(ref); - for (int i = 0; i < favorites->childCount(); ++i) { - QTreeWidgetItem* child = favorites->child(i); - if (child && child->data(0, Qt::UserRole).toString() == ref) { - favorites->removeChild(child); - } - } - } else { // Add to favorites - QPushButton *favBtn = new QPushButton(); - btn->setIcon(iconFilled); - mapFavs->insert(ref, {btn, favBtn}); - favRefs->append(ref); - addChildItem(displayName, ref, favorites, favBtn, true); - } - - const QString favs =favRefs->join(";"); - params.put("ModelManager_Favs", favs.toStdString()); -} - -/** - * Adds a child item to a given folder item within the QTreeWidget. - * - * @param displayName The text to display for the child item. - * @param ref A reference string that uniquely identifies the child item. - * @param folderItem The parent folder item to which the child item will be added. - * @param btn A pointer to a QPushButton associated with the child item. If nullptr, a new button will be created. - * @param addAtTop If true, the child item is added as the first child of the folder item; otherwise, it is appended to the end. - * @return A pointer to the created QTreeWidgetItem representing the child item. - */ -QTreeWidgetItem* TreeOptionDialog::addChildItem(const QString &displayName, const QString &ref, QTreeWidgetItem *folderItem, QPushButton *btn, bool addAtTop) { - QTreeWidgetItem *childItem = new QTreeWidgetItem(); - if (btn == nullptr) { - btn = new QPushButton(); - } - if (mapFavs->keys().contains(ref)) { - btn->setIcon(iconFilled); - (*mapFavs)[ref].append(btn); - } else { - btn->setIcon(iconBlank); - } - btn->setIconSize(QSize(100, 100)); - QWidget *buttonContainer = new QWidget(); - QHBoxLayout *layout = new QHBoxLayout(buttonContainer); - layout->addWidget(btn, 0, Qt::AlignRight); - childItem->setText(0, displayName); - childItem->setData(0, Qt::UserRole, ref); - childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable); - if (addAtTop) { - folderItem->insertChild(0, childItem); - } else { - folderItem->addChild(childItem); - } - treeWidget->setItemWidget(childItem, 0, buttonContainer); - - connect(btn, &QPushButton::clicked, btn, [=]() { - handleFavorites(displayName, ref, btn); - }); - return childItem; -} diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h deleted file mode 100644 index 3fb1ebfe1a..0000000000 --- a/selfdrive/ui/qt/widgets/input.h +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "common/params.h" -#include "selfdrive/ui/qt/widgets/keyboard.h" - - -struct TreeNode { - QString folder; - QString displayName; - QString ref; - int index; -}; - -struct TreeFolder { - QString folder; - QList items; -}; - -class DialogBase : public QDialog { - Q_OBJECT - -protected: - DialogBase(QWidget *parent); - bool eventFilter(QObject *o, QEvent *e) override; - -public slots: - int exec() override; -}; - -class InputDialog : public DialogBase { - Q_OBJECT - -public: - explicit InputDialog(const QString &title, QWidget *parent, const QString &subtitle = "", bool secret = false); - static QString getText(const QString &title, QWidget *parent, const QString &subtitle = "", - bool secret = false, int minLength = -1, const QString &defaultText = ""); - QString text(); - void setMessage(const QString &message, bool clearInputField = true); - void setMinLength(int length); - void show(); - -private: - int minLength; - QLineEdit *line; - Keyboard *k; - QLabel *label; - QLabel *sublabel; - QVBoxLayout *main_layout; - QPushButton *eye_btn; - -private slots: - void handleEnter(); - -signals: - void cancel(); - void emitText(const QString &text); -}; - -class ConfirmationDialog : public DialogBase { - Q_OBJECT - -public: - explicit ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, - const QString &cancel_text, const bool rich, QWidget* parent); - static bool alert(const QString &prompt_text, QWidget *parent); - static bool confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent); - static bool rich(const QString &prompt_text, QWidget *parent); -}; - -class MultiOptionDialog : public DialogBase { - Q_OBJECT - -public: - explicit MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); - static QString getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); - QString selection; -}; - -class TreeOptionDialog : public DialogBase { - Q_OBJECT - -public: - explicit TreeOptionDialog(const QString &prompt_text, const QList &items, const QString ¤t, const QString &favParam, QWidget *parent = nullptr); - static QString getSelection(const QString &prompt_text, const QList &items, const QString ¤t, const QString &favParam, QWidget *parent = nullptr); - void handleFavorites(const QString &displayName, const QString &ref, QPushButton* btn); - QTreeWidgetItem* addChildItem(const QString &displayName, const QString &ref, QTreeWidgetItem* folderItem, QPushButton* btn = nullptr, bool addAtTop = false); - QString selection; - -private: - QTreeWidget *treeWidget; - QPushButton *confirm_btn; - Params params; - QMap> *mapFavs; - QStringList *favRefs; - QTreeWidgetItem *favorites; - - QIcon iconBlank; - QIcon iconFilled; -}; diff --git a/selfdrive/ui/qt/widgets/keyboard.cc b/selfdrive/ui/qt/widgets/keyboard.cc deleted file mode 100644 index 9ead27b8d5..0000000000 --- a/selfdrive/ui/qt/widgets/keyboard.cc +++ /dev/null @@ -1,182 +0,0 @@ -#include "selfdrive/ui/qt/widgets/keyboard.h" - -#include - -#include -#include -#include -#include -#include - -const QString BACKSPACE_KEY = "⌫"; -const QString ENTER_KEY = "→"; -const QString SHIFT_KEY = "⇧"; -const QString CAPS_LOCK_KEY = "⇪"; - -const QMap KEY_STRETCH = {{" ", 3}, {ENTER_KEY, 2}}; - -const QStringList CONTROL_BUTTONS = {SHIFT_KEY, CAPS_LOCK_KEY, "ABC", "#+=", "123", BACKSPACE_KEY, ENTER_KEY}; - -const float key_spacing_vertical = 20; -const float key_spacing_horizontal = 15; - -KeyButton::KeyButton(const QString &text, QWidget *parent) : QPushButton(text, parent) { - setAttribute(Qt::WA_AcceptTouchEvents); - setFocusPolicy(Qt::NoFocus); -} - -bool KeyButton::event(QEvent *event) { - if (event->type() == QEvent::TouchBegin || event->type() == QEvent::TouchEnd) { - QTouchEvent *touchEvent = static_cast(event); - if (!touchEvent->touchPoints().empty()) { - const QEvent::Type mouseType = event->type() == QEvent::TouchBegin ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease; - QMouseEvent mouseEvent(mouseType, touchEvent->touchPoints().front().pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); - QPushButton::event(&mouseEvent); - event->accept(); - parentWidget()->update(); - return true; - } - } - return QPushButton::event(event); -} - -KeyboardLayout::KeyboardLayout(QWidget* parent, const std::vector>& layout) : QWidget(parent) { - QVBoxLayout* main_layout = new QVBoxLayout(this); - main_layout->setMargin(0); - main_layout->setSpacing(0); - - QButtonGroup* btn_group = new QButtonGroup(this); - QObject::connect(btn_group, SIGNAL(buttonClicked(QAbstractButton*)), parent, SLOT(handleButton(QAbstractButton*))); - - for (const auto &s : layout) { - QHBoxLayout *hlayout = new QHBoxLayout; - hlayout->setSpacing(0); - - if (main_layout->count() == 1) { - hlayout->addSpacing(90); - } - - for (const QString &p : s) { - KeyButton* btn = new KeyButton(p); - if (p == BACKSPACE_KEY) { - btn->setAutoRepeat(true); - } else if (p == ENTER_KEY) { - btn->setStyleSheet(R"( - QPushButton { - background-color: #465BEA; - } - QPushButton:pressed { - background-color: #444444; - } - )"); - } - btn->setFixedHeight(135 + key_spacing_vertical); - btn_group->addButton(btn); - hlayout->addWidget(btn, KEY_STRETCH.value(p, 1)); - } - - if (main_layout->count() == 1) { - hlayout->addSpacing(90); - } - - main_layout->addLayout(hlayout); - } - - setStyleSheet(QString(R"( - QPushButton { - font-size: 75px; - margin-left: %1px; - margin-right: %1px; - margin-top: %2px; - margin-bottom: %2px; - padding: 0px; - border-radius: 10px; - color: #dddddd; - background-color: #444444; - } - QPushButton:pressed { - background-color: #333333; - } - )").arg(key_spacing_vertical / 2).arg(key_spacing_horizontal / 2)); -} - -Keyboard::Keyboard(QWidget *parent) : QFrame(parent) { - main_layout = new QStackedLayout(this); - main_layout->setMargin(0); - - // lowercase - std::vector> lowercase = { - {"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}, - {"a", "s", "d", "f", "g", "h", "j", "k", "l"}, - {SHIFT_KEY, "z", "x", "c", "v", "b", "n", "m", BACKSPACE_KEY}, - {"123", "/", "-", " ", ".", ENTER_KEY}, - }; - main_layout->addWidget(new KeyboardLayout(this, lowercase)); - - // uppercase - std::vector> uppercase = { - {"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"}, - {"A", "S", "D", "F", "G", "H", "J", "K", "L"}, - {SHIFT_KEY, "Z", "X", "C", "V", "B", "N", "M", BACKSPACE_KEY}, - {"123", "/", "-", " ", ".", ENTER_KEY}, - }; - main_layout->addWidget(new KeyboardLayout(this, uppercase)); - - // numbers + specials - std::vector> numbers = { - {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}, - {"-", "/", ":", ";", "(", ")", "$", "&&", "@", "\""}, - {"#+=", ".", ",", "?", "!", "`", BACKSPACE_KEY}, - {"ABC", " ", ".", ENTER_KEY}, - }; - main_layout->addWidget(new KeyboardLayout(this, numbers)); - - // extra specials - std::vector> specials = { - {"[", "]", "{", "}", "#", "%", "^", "*", "+", "="}, - {"_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"}, - {"123", ".", ",", "?", "!", "'", BACKSPACE_KEY}, - {"ABC", " ", ".", ENTER_KEY}, - }; - main_layout->addWidget(new KeyboardLayout(this, specials)); - - main_layout->setCurrentIndex(0); -} - -void Keyboard::handleCapsPress() { - shift_state = (shift_state + 1) % 3; - bool is_uppercase = shift_state > 0; - main_layout->setCurrentIndex(is_uppercase); - - for (KeyButton* btn : main_layout->currentWidget()->findChildren()) { - if (btn->text() == SHIFT_KEY || btn->text() == CAPS_LOCK_KEY) { - btn->setText(shift_state == 2 ? CAPS_LOCK_KEY : SHIFT_KEY); - btn->setStyleSheet(is_uppercase ? "background-color: #465BEA;" : ""); - } - } -} - -void Keyboard::handleButton(QAbstractButton* btn) { - const QString &key = btn->text(); - if (CONTROL_BUTTONS.contains(key)) { - if (key == "ABC" || key == "123" || key == "#+=") { - int index = (key == "ABC") ? 0 : (key == "123" ? 2 : 3); - main_layout->setCurrentIndex(index); - shift_state = 0; - } else if (key == SHIFT_KEY || key == CAPS_LOCK_KEY) { - handleCapsPress(); - } else if (key == ENTER_KEY) { - main_layout->setCurrentIndex(0); - shift_state = 0; - emit emitEnter(); - } else if (key == BACKSPACE_KEY) { - emit emitBackspace(); - } - } else { - if (shift_state == 1 && "A" <= key && key <= "Z") { - main_layout->setCurrentIndex(0); - shift_state = 0; - } - emit emitKey(key); - } -} diff --git a/selfdrive/ui/qt/widgets/keyboard.h b/selfdrive/ui/qt/widgets/keyboard.h deleted file mode 100644 index e61617283a..0000000000 --- a/selfdrive/ui/qt/widgets/keyboard.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -class KeyButton : public QPushButton { - Q_OBJECT - -public: - KeyButton(const QString &text, QWidget *parent = 0); - bool event(QEvent *event) override; -}; - -class KeyboardLayout : public QWidget { - Q_OBJECT - -public: - explicit KeyboardLayout(QWidget* parent, const std::vector>& layout); -}; - -class Keyboard : public QFrame { - Q_OBJECT - -public: - explicit Keyboard(QWidget *parent = 0); - -private: - QStackedLayout* main_layout; - int shift_state = 0; - -private slots: - void handleButton(QAbstractButton* m_button); - void handleCapsPress(); - -signals: - void emitKey(const QString &s); - void emitBackspace(); - void emitEnter(); -}; diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.cc b/selfdrive/ui/qt/widgets/offroad_alerts.cc deleted file mode 100644 index 3a4828a2a8..0000000000 --- a/selfdrive/ui/qt/widgets/offroad_alerts.cc +++ /dev/null @@ -1,138 +0,0 @@ -#include "selfdrive/ui/qt/widgets/offroad_alerts.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include "common/util.h" -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/widgets/scrollview.h" - -AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setMargin(50); - main_layout->setSpacing(30); - - QWidget *widget = new QWidget; - scrollable_layout = new QVBoxLayout(widget); - widget->setStyleSheet("background-color: transparent;"); - main_layout->addWidget(new ScrollView(widget)); - - // bottom footer, dismiss + reboot buttons - QHBoxLayout *footer_layout = new QHBoxLayout(); - main_layout->addLayout(footer_layout); - - QPushButton *dismiss_btn = new QPushButton(tr("Close")); - dismiss_btn->setFixedSize(400, 125); - footer_layout->addWidget(dismiss_btn, 0, Qt::AlignBottom | Qt::AlignLeft); - QObject::connect(dismiss_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); - - action_btn = new QPushButton(); - action_btn->setVisible(false); - action_btn->setFixedHeight(125); - footer_layout->addWidget(action_btn, 0, Qt::AlignBottom | Qt::AlignRight); - QObject::connect(action_btn, &QPushButton::clicked, [=]() { - if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { - params.remove("Offroad_ExcessiveActuation"); - } else { - params.putBool("SnoozeUpdate", true); - } - }); - QObject::connect(action_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); - action_btn->setStyleSheet("color: white; background-color: #4F4F4F; padding-left: 60px; padding-right: 60px;"); - - if (hasRebootBtn) { - QPushButton *rebootBtn = new QPushButton(tr("Reboot and Update")); - rebootBtn->setFixedSize(600, 125); - footer_layout->addWidget(rebootBtn, 0, Qt::AlignBottom | Qt::AlignRight); - QObject::connect(rebootBtn, &QPushButton::clicked, [=]() { Hardware::reboot(); }); - } - - setStyleSheet(R"( - * { - font-size: 48px; - color: white; - } - QFrame { - border-radius: 30px; - background-color: #393939; - } - QPushButton { - color: black; - font-weight: 500; - border-radius: 30px; - background-color: white; - } - )"); -} - -int OffroadAlert::refresh() { - // build widgets for each offroad alert on first refresh - if (alerts.empty()) { - QString json = util::read_file("../selfdrived/alerts_offroad.json").c_str(); - QJsonObject obj = QJsonDocument::fromJson(json.toUtf8()).object(); - - // descending sort labels by severity - std::vector> sorted; - for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { - sorted.push_back({it.key().toStdString(), it.value()["severity"].toInt()}); - } - std::sort(sorted.begin(), sorted.end(), [=](auto &l, auto &r) { return l.second > r.second; }); - - for (auto &[key, severity] : sorted) { - QLabel *l = new QLabel(this); - alerts[key] = l; - l->setMargin(60); - l->setWordWrap(true); - l->setStyleSheet(QString("background-color: %1").arg(severity ? "#E22C2C" : "#292929")); - scrollable_layout->addWidget(l); - } - scrollable_layout->addStretch(1); - } - - int alertCount = 0; - for (const auto &[key, label] : alerts) { - QString text; - std::string bytes = params.get(key); - if (bytes.size()) { - auto doc_par = QJsonDocument::fromJson(bytes.c_str()); - text = tr(doc_par["text"].toString().toUtf8().data()); - auto extra = doc_par["extra"].toString(); - if (!extra.isEmpty()) { - text = text.arg(extra); - } - } - label->setText(text); - label->setVisible(!text.isEmpty()); - alertCount += !text.isEmpty(); - } - - action_btn->setVisible(!alerts["Offroad_ExcessiveActuation"]->text().isEmpty() || !alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); - if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { - action_btn->setText(tr("Acknowledge Excessive Actuation")); - } else { - action_btn->setText(tr("Snooze Update")); - } - - return alertCount; -} - -UpdateAlert::UpdateAlert(QWidget *parent) : AbstractAlert(true, parent) { - releaseNotes = new QLabel(this); - releaseNotes->setWordWrap(true); - releaseNotes->setAlignment(Qt::AlignTop); - scrollable_layout->addWidget(releaseNotes); -} - -bool UpdateAlert::refresh() { - bool updateAvailable = params.getBool("UpdateAvailable"); - if (updateAvailable) { - releaseNotes->setText(params.get("UpdaterNewReleaseNotes").c_str()); - } - return updateAvailable; -} diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.h b/selfdrive/ui/qt/widgets/offroad_alerts.h deleted file mode 100644 index 2dcf4f9d8c..0000000000 --- a/selfdrive/ui/qt/widgets/offroad_alerts.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -#include "common/params.h" - -class AbstractAlert : public QFrame { - Q_OBJECT - -protected: - AbstractAlert(bool hasRebootBtn, QWidget *parent = nullptr); - - QPushButton *action_btn; - QVBoxLayout *scrollable_layout; - Params params; - std::map alerts; - -signals: - void dismiss(); -}; - -class UpdateAlert : public AbstractAlert { - Q_OBJECT - -public: - UpdateAlert(QWidget *parent = 0); - bool refresh(); - -private: - QLabel *releaseNotes = nullptr; -}; - -class OffroadAlert : public AbstractAlert { - Q_OBJECT - -public: - explicit OffroadAlert(QWidget *parent = 0) : AbstractAlert(false, parent) {} - int refresh(); -}; diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc deleted file mode 100644 index a69580ea50..0000000000 --- a/selfdrive/ui/qt/widgets/prime.cc +++ /dev/null @@ -1,265 +0,0 @@ -#include "selfdrive/ui/qt/widgets/prime.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "selfdrive/ui/qt/request_repeater.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/widgets/wifi.h" - -using qrcodegen::QrCode; - -PairingQRWidget::PairingQRWidget(QWidget* parent) : QWidget(parent) { - timer = new QTimer(this); - connect(timer, &QTimer::timeout, this, &PairingQRWidget::refresh); -} - -void PairingQRWidget::showEvent(QShowEvent *event) { - refresh(); - timer->start(5 * 60 * 1000); - device()->setOffroadBrightness(100); -} - -void PairingQRWidget::hideEvent(QHideEvent *event) { - timer->stop(); - device()->setOffroadBrightness(BACKLIGHT_OFFROAD); -} - -void PairingQRWidget::refresh() { - QString pairToken = CommaApi::create_jwt({{"pair", true}}); - QString qrString = "https://connect.comma.ai/?pair=" + pairToken; - this->updateQrCode(qrString); - update(); -} - -void PairingQRWidget::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 PairingQRWidget::paintEvent(QPaintEvent *e) { - QPainter p(this); - p.fillRect(rect(), Qt::white); - - QSize s = (size() - img.size()) / 2; - p.drawPixmap(s.width(), s.height(), img); -} - - -PairingPopup::PairingPopup(QWidget *parent) : DialogBase(parent) { - QHBoxLayout *hlayout = new QHBoxLayout(this); - hlayout->setContentsMargins(0, 0, 0, 0); - hlayout->setSpacing(0); - - setStyleSheet("PairingPopup { background-color: #E0E0E0; }"); - - // text - QVBoxLayout *vlayout = new QVBoxLayout(); - vlayout->setContentsMargins(85, 70, 50, 70); - vlayout->setSpacing(50); - hlayout->addLayout(vlayout, 1); - { - QPushButton *close = new QPushButton(QIcon(":/icons/close.svg"), "", this); - close->setIconSize(QSize(80, 80)); - close->setStyleSheet("border: none;"); - vlayout->addWidget(close, 0, Qt::AlignLeft); - QObject::connect(close, &QPushButton::clicked, this, &QDialog::reject); - - vlayout->addSpacing(30); - - QLabel *title = new QLabel(tr("Pair your device to your comma account"), this); - title->setStyleSheet("font-size: 75px; color: black;"); - title->setWordWrap(true); - vlayout->addWidget(title); - - QLabel *instructions = new QLabel(QString(R"( -
    -
  1. %1
  2. -
  3. %2
  4. -
  5. %3
  6. -
- )").arg(tr("Go to https://connect.comma.ai on your phone")) - .arg(tr("Click \"add new device\" and scan the QR code on the right")) - .arg(tr("Bookmark connect.comma.ai to your home screen to use it like an app")), this); - - instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); - instructions->setWordWrap(true); - vlayout->addWidget(instructions); - - vlayout->addStretch(); - } - - // QR code - PairingQRWidget *qr = new PairingQRWidget(this); - hlayout->addWidget(qr, 1); -} - -int PairingPopup::exec() { - if (!util::system_time_valid()) { - ConfirmationDialog::alert(tr("Please connect to Wi-Fi to complete initial pairing"), parentWidget()); - return QDialog::Rejected; - } - return DialogBase::exec(); -} - - -PrimeUserWidget::PrimeUserWidget(QWidget *parent) : QFrame(parent) { - setObjectName("primeWidget"); - QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(56, 40, 56, 40); - mainLayout->setSpacing(20); - - QLabel *subscribed = new QLabel(tr("✓ SUBSCRIBED")); - subscribed->setStyleSheet("font-size: 41px; font-weight: bold; color: #86FF4E;"); - mainLayout->addWidget(subscribed); - - QLabel *commaPrime = new QLabel(tr("comma prime")); - commaPrime->setStyleSheet("font-size: 75px; font-weight: bold;"); - mainLayout->addWidget(commaPrime); -} - - -PrimeAdWidget::PrimeAdWidget(QWidget* parent) : QFrame(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(80, 90, 80, 60); - main_layout->setSpacing(0); - - QLabel *upgrade = new QLabel(tr("Upgrade Now")); - upgrade->setStyleSheet("font-size: 75px; font-weight: bold;"); - main_layout->addWidget(upgrade, 0, Qt::AlignTop); - main_layout->addSpacing(50); - - QLabel *description = new QLabel(tr("Become a comma prime member at connect.comma.ai")); - description->setStyleSheet("font-size: 56px; font-weight: light; color: white;"); - description->setWordWrap(true); - main_layout->addWidget(description, 0, Qt::AlignTop); - - main_layout->addStretch(); - - QLabel *features = new QLabel(tr("PRIME FEATURES:")); - features->setStyleSheet("font-size: 41px; font-weight: bold; color: #E5E5E5;"); - main_layout->addWidget(features, 0, Qt::AlignBottom); - main_layout->addSpacing(30); - - QVector bullets = {tr("Remote access"), tr("24/7 LTE connectivity"), tr("1 year of drive storage"), tr("Remote snapshots")}; - for (auto &b : bullets) { - const QString check = " "; - QLabel *l = new QLabel(check + b); - l->setAlignment(Qt::AlignLeft); - l->setStyleSheet("font-size: 50px; margin-bottom: 15px;"); - main_layout->addWidget(l, 0, Qt::AlignBottom); - } - - setStyleSheet(R"( - PrimeAdWidget { - border-radius: 10px; - background-color: #333333; - } - )"); -} - - -SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { - mainLayout = new QStackedWidget; - - // Unpaired, registration prompt layout - - QFrame* finishRegistration = new QFrame; - finishRegistration->setObjectName("primeWidget"); - QVBoxLayout* finishRegistrationLayout = new QVBoxLayout(finishRegistration); - finishRegistrationLayout->setSpacing(38); - finishRegistrationLayout->setContentsMargins(64, 48, 64, 48); - - QLabel* registrationTitle = new QLabel(tr("Finish Setup")); - registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold;"); - finishRegistrationLayout->addWidget(registrationTitle); - - QLabel* registrationDescription = new QLabel(tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); - registrationDescription->setWordWrap(true); - registrationDescription->setStyleSheet("font-size: 50px; font-weight: light;"); - finishRegistrationLayout->addWidget(registrationDescription); - - finishRegistrationLayout->addStretch(); - - QPushButton* pair = new QPushButton(tr("Pair device")); - pair->setStyleSheet(R"( - QPushButton { - font-size: 55px; - font-weight: 500; - border-radius: 10px; - background-color: #465BEA; - padding: 64px; - } - QPushButton:pressed { - background-color: #3049F4; - } - )"); - finishRegistrationLayout->addWidget(pair); - - popup = new PairingPopup(this); - QObject::connect(pair, &QPushButton::clicked, popup, &PairingPopup::exec); - - mainLayout->addWidget(finishRegistration); - - // build stacked layout - QVBoxLayout *outer_layout = new QVBoxLayout(this); - outer_layout->setContentsMargins(0, 0, 0, 0); - outer_layout->addWidget(mainLayout); - - QWidget *content = new QWidget; - content_layout = new QVBoxLayout(content); - content_layout->setContentsMargins(0, 0, 0, 0); - content_layout->setSpacing(30); - - WiFiPromptWidget *wifi_prompt = new WiFiPromptWidget; - QObject::connect(wifi_prompt, &WiFiPromptWidget::openSettings, this, &SetupWidget::openSettings); - content_layout->addWidget(wifi_prompt); - content_layout->addStretch(); - - mainLayout->addWidget(content); - - mainLayout->setCurrentIndex(1); - - setStyleSheet(R"( - #primeWidget { - border-radius: 10px; - background-color: #333333; - } - )"); - - // Retain size while hidden - QSizePolicy sp_retain = sizePolicy(); - sp_retain.setRetainSizeWhenHidden(true); - setSizePolicy(sp_retain); - - QObject::connect(uiState()->prime_state, &PrimeState::changed, [this](PrimeState::Type type) { - if (type == PrimeState::PRIME_TYPE_UNPAIRED) { - mainLayout->setCurrentIndex(0); // Display "Pair your device" widget - } else { - popup->reject(); - mainLayout->setCurrentIndex(1); // Display Wi-Fi prompt widget - } - }); -} diff --git a/selfdrive/ui/qt/widgets/prime.h b/selfdrive/ui/qt/widgets/prime.h deleted file mode 100644 index 53d743398d..0000000000 --- a/selfdrive/ui/qt/widgets/prime.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "selfdrive/ui/qt/widgets/input.h" - -// pairing QR code -class PairingQRWidget : public QWidget { - Q_OBJECT - -public: - explicit PairingQRWidget(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; - -private slots: - void refresh(); -}; - - -// pairing popup widget -class PairingPopup : public DialogBase { - Q_OBJECT - -public: - explicit PairingPopup(QWidget* parent); - int exec() override; -}; - - -// widget for paired users with prime -class PrimeUserWidget : public QFrame { - Q_OBJECT - -public: - explicit PrimeUserWidget(QWidget* parent = 0); -}; - - -// widget for paired users without prime -class PrimeAdWidget : public QFrame { - Q_OBJECT -public: - explicit PrimeAdWidget(QWidget* parent = 0); -}; - - -// container widget -class SetupWidget : public QFrame { - Q_OBJECT - -public: - explicit SetupWidget(QWidget* parent = 0); - -signals: - void openSettings(int index = 0, const QString ¶m = ""); - -protected: - QVBoxLayout *content_layout; - -private: - PairingPopup *popup; - QStackedWidget *mainLayout; -}; diff --git a/selfdrive/ui/qt/widgets/scrollview.cc b/selfdrive/ui/qt/widgets/scrollview.cc deleted file mode 100644 index 978bf83a63..0000000000 --- a/selfdrive/ui/qt/widgets/scrollview.cc +++ /dev/null @@ -1,49 +0,0 @@ -#include "selfdrive/ui/qt/widgets/scrollview.h" - -#include -#include - -// TODO: disable horizontal scrolling and resize - -ScrollView::ScrollView(QWidget *w, QWidget *parent) : QScrollArea(parent) { - setWidget(w); - setWidgetResizable(true); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setStyleSheet("background-color: transparent; border:none"); - - QString style = R"( - QScrollBar:vertical { - border: none; - background: transparent; - width: 10px; - margin: 0; - } - QScrollBar::handle:vertical { - min-height: 0px; - border-radius: 5px; - background-color: white; - } - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { - height: 0px; - } - QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: none; - } - )"; - verticalScrollBar()->setStyleSheet(style); - horizontalScrollBar()->setStyleSheet(style); - - QScroller *scroller = QScroller::scroller(this->viewport()); - QScrollerProperties sp = scroller->scrollerProperties(); - - sp.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); - sp.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); - sp.setScrollMetric(QScrollerProperties::MousePressEventDelay, 0.01); - scroller->grabGesture(this->viewport(), QScroller::LeftMouseButtonGesture); - scroller->setScrollerProperties(sp); -} - -void ScrollView::hideEvent(QHideEvent *e) { - verticalScrollBar()->setValue(0); -} diff --git a/selfdrive/ui/qt/widgets/scrollview.h b/selfdrive/ui/qt/widgets/scrollview.h deleted file mode 100644 index 024331aa39..0000000000 --- a/selfdrive/ui/qt/widgets/scrollview.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -class ScrollView : public QScrollArea { - Q_OBJECT - -public: - explicit ScrollView(QWidget *w = nullptr, QWidget *parent = nullptr); -protected: - void hideEvent(QHideEvent *e) override; -}; diff --git a/selfdrive/ui/qt/widgets/ssh_keys.cc b/selfdrive/ui/qt/widgets/ssh_keys.cc deleted file mode 100644 index 26743952de..0000000000 --- a/selfdrive/ui/qt/widgets/ssh_keys.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include "selfdrive/ui/qt/widgets/ssh_keys.h" - -#include "common/params.h" -#include "selfdrive/ui/qt/api.h" -#include "selfdrive/ui/qt/widgets/input.h" - -SshControl::SshControl() : - ButtonControl(tr("SSH Keys"), "", tr("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.")) { - - QObject::connect(this, &ButtonControl::clicked, [=]() { - if (text() == tr("ADD")) { - QString username = InputDialog::getText(tr("Enter your GitHub username"), this); - if (username.length() > 0) { - setText(tr("LOADING")); - setEnabled(false); - getUserKeys(username); - } - } else { - params.remove("GithubUsername"); - params.remove("GithubSshKeys"); - refresh(); - } - }); - - refresh(); -} - -void SshControl::refresh() { - QString param = QString::fromStdString(params.get("GithubSshKeys")); - if (param.length()) { - setValue(QString::fromStdString(params.get("GithubUsername"))); - setText(tr("REMOVE")); - } else { - setValue(""); - setText(tr("ADD")); - } - setEnabled(true); -} - -void SshControl::getUserKeys(const QString &username) { - HttpRequest *request = new HttpRequest(this, false); - QObject::connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) { - if (success) { - if (!resp.isEmpty()) { - params.put("GithubUsername", username.toStdString()); - params.put("GithubSshKeys", resp.toStdString()); - } else { - ConfirmationDialog::alert(tr("Username '%1' has no keys on GitHub").arg(username), this); - } - } else { - if (request->timeout()) { - ConfirmationDialog::alert(tr("Request timed out"), this); - } else { - ConfirmationDialog::alert(tr("Username '%1' doesn't exist on GitHub").arg(username), this); - } - } - - refresh(); - request->deleteLater(); - }); - - request->sendRequest("https://github.com/" + username + ".keys"); -} diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h deleted file mode 100644 index ef40346c83..0000000000 --- a/selfdrive/ui/qt/widgets/ssh_keys.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#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 { - Q_OBJECT - -public: - SshToggle() : ToggleControl(tr("Enable SSH"), "", "", Hardware::get_ssh_enabled()) { - QObject::connect(this, &SshToggle::toggleFlipped, [=](bool state) { - Hardware::set_ssh_enabled(state); - }); - } -}; - -// SSH key management widget -class SshControl : public ButtonControl { - Q_OBJECT - -public: - SshControl(); - -private: - Params params; - - void refresh(); - void getUserKeys(const QString &username); -}; diff --git a/selfdrive/ui/qt/widgets/toggle.cc b/selfdrive/ui/qt/widgets/toggle.cc deleted file mode 100644 index 82302ad5bc..0000000000 --- a/selfdrive/ui/qt/widgets/toggle.cc +++ /dev/null @@ -1,83 +0,0 @@ -#include "selfdrive/ui/qt/widgets/toggle.h" - -#include - -Toggle::Toggle(QWidget *parent) : QAbstractButton(parent), -_height(80), -_height_rect(60), -on(false), -_anim(new QPropertyAnimation(this, "offset_circle", this)) -{ - _radius = _height / 2; - _x_circle = _radius; - _y_circle = _radius; - _y_rect = (_height - _height_rect)/2; - circleColor = QColor(0xffffff); // placeholder - green = QColor(0xffffff); // placeholder - setEnabled(true); -} - -void Toggle::paintEvent(QPaintEvent *e) { - this->setFixedHeight(_height); - QPainter p(this); - p.setPen(Qt::NoPen); - p.setRenderHint(QPainter::Antialiasing, true); - - // Draw toggle background left - p.setBrush(green); - p.drawRoundedRect(QRect(0, _y_rect, _x_circle + _radius, _height_rect), _height_rect/2, _height_rect/2); - - // Draw toggle background right - p.setBrush(QColor(0x393939)); - p.drawRoundedRect(QRect(_x_circle - _radius, _y_rect, width() - (_x_circle - _radius), _height_rect), _height_rect/2, _height_rect/2); - - // Draw toggle circle - p.setBrush(circleColor); - p.drawEllipse(QRectF(_x_circle - _radius, _y_circle - _radius, 2 * _radius, 2 * _radius)); -} - -void Toggle::mouseReleaseEvent(QMouseEvent *e) { - if (!enabled) { - return; - } - const int left = _radius; - const int right = width() - _radius; - if ((_x_circle != left && _x_circle != right) || !this->rect().contains(e->localPos().toPoint())) { - // If mouse release isn't in rect or animation is running, don't parse touch events - return; - } - if (e->button() & Qt::LeftButton) { - togglePosition(); - emit stateChanged(on); - } -} - -void Toggle::togglePosition() { - on = !on; - const int left = _radius; - const int right = width() - _radius; - _anim->setStartValue(on ? left + immediateOffset : right - immediateOffset); - _anim->setEndValue(on ? right : left); - _anim->setDuration(animation_duration); - _anim->start(); - repaint(); -} - -void Toggle::enterEvent(QEvent *e) { - QAbstractButton::enterEvent(e); -} - -bool Toggle::getEnabled() { - return enabled; -} - -void Toggle::setEnabled(bool value) { - enabled = value; - if (value) { - circleColor.setRgb(0xfafafa); - green.setRgb(0x33ab4c); - } else { - circleColor.setRgb(0x888888); - green.setRgb(0x227722); - } -} diff --git a/selfdrive/ui/qt/widgets/toggle.h b/selfdrive/ui/qt/widgets/toggle.h deleted file mode 100644 index a0fa434a4c..0000000000 --- a/selfdrive/ui/qt/widgets/toggle.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include -#include - -class Toggle : public QAbstractButton { - Q_OBJECT - Q_PROPERTY(int offset_circle READ offset_circle WRITE set_offset_circle CONSTANT) - -public: - Toggle(QWidget* parent = nullptr); - void togglePosition(); - bool on; - int animation_duration = 150; - int immediateOffset = 0; - int offset_circle() const { - return _x_circle; - } - - void set_offset_circle(int o) { - _x_circle = o; - update(); - } - bool getEnabled(); - virtual void setEnabled(bool value); - -protected: - void paintEvent(QPaintEvent*) override; - void mouseReleaseEvent(QMouseEvent*) override; - void enterEvent(QEvent*) override; - - QColor circleColor; - QColor green; - bool enabled = true; - int _x_circle, _y_circle; - int _height, _radius; - int _height_rect, _y_rect; - QPropertyAnimation *_anim = nullptr; - -signals: - void stateChanged(bool new_state); -}; diff --git a/selfdrive/ui/qt/widgets/wifi.cc b/selfdrive/ui/qt/widgets/wifi.cc deleted file mode 100644 index 8958e48c93..0000000000 --- a/selfdrive/ui/qt/widgets/wifi.cc +++ /dev/null @@ -1,46 +0,0 @@ -#include "selfdrive/ui/qt/widgets/wifi.h" - -#include -#include -#include -#include - -WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) { - // Setup Firehose Mode - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(56, 40, 56, 40); - main_layout->setSpacing(42); - - community_popup = new SunnylinkCommunityPopup(this); - QLabel *title = new QLabel(tr("sunnypilot Community")); - title->setStyleSheet("font-size: 56px; font-weight: 500;"); - main_layout->addWidget(title); - - QLabel *desc = new QLabel(tr("Need help or have ideas?
Join our community now!")); - desc->setStyleSheet("font-size: 40px; font-weight: 400;"); - desc->setWordWrap(true); - main_layout->addWidget(desc); - - QPushButton *settings_btn = new QPushButton(tr("Learn More")); - connect(settings_btn, &QPushButton::clicked, [=]() { community_popup->exec(); }); - settings_btn->setStyleSheet(R"( - QPushButton { - font-size: 48px; - font-weight: 500; - border-radius: 10px; - background-color: #465BEA; - padding: 32px; - } - QPushButton:pressed { - background-color: #3049F4; - } - )"); - main_layout->addWidget(settings_btn); - - setStyleSheet(R"( - WiFiPromptWidget { - background-color: #333333; - border-radius: 10px; - } - )"); -} \ No newline at end of file diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h deleted file mode 100644 index 8ef9dca91a..0000000000 --- a/selfdrive/ui/qt/widgets/wifi.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h" - -class WiFiPromptWidget : public QFrame { - Q_OBJECT - -public: - explicit WiFiPromptWidget(QWidget* parent = 0); - -private: - SunnylinkCommunityPopup *community_popup; - -signals: - void openSettings(int index = 0, const QString ¶m = ""); -}; diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc deleted file mode 100644 index d0806e4379..0000000000 --- a/selfdrive/ui/qt/window.cc +++ /dev/null @@ -1,111 +0,0 @@ -#include "selfdrive/ui/qt/window.h" - -#include - -#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) : - QWidget(parent), - homeWindow(hw ? hw : new HomeWindow(this)), - settingsWindow(sw ? sw : new SettingsWindow(this)) { - - main_layout = new QStackedLayout(this); - main_layout->setMargin(0); - - main_layout->addWidget(homeWindow); - QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); - QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); - - main_layout->addWidget(settingsWindow); - QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings); - QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() { - onboardingWindow->showTrainingGuide(); - main_layout->setCurrentWidget(onboardingWindow); - }); - QObject::connect(settingsWindow, &SettingsWindow::showDriverView, [=] { - homeWindow->showDriverView(true); - }); - - onboardingWindow = new OnboardingWindow(this); - main_layout->addWidget(onboardingWindow); - QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() { - main_layout->setCurrentWidget(homeWindow); - }); - if (!onboardingWindow->completed()) { - main_layout->setCurrentWidget(onboardingWindow); - } - - QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { - if (!offroad) { - closeSettings(); - } - }); - QObject::connect(device(), &Device::interactiveTimeout, [=]() { - if (main_layout->currentWidget() == settingsWindow) { - closeSettings(); - } - }); - - // load fonts - QFontDatabase::addApplicationFont("../assets/fonts/Inter-Black.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/Inter-Bold.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraBold.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraLight.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/Inter-Medium.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/Inter-Regular.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/Inter-SemiBold.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/Inter-Thin.ttf"); - QFontDatabase::addApplicationFont("../assets/fonts/JetBrainsMono-Medium.ttf"); - - // no outline to prevent the focus rectangle - setStyleSheet(R"( - * { - font-family: Inter; - outline: none; - } - )"); - setAttribute(Qt::WA_NoSystemBackground); -} - -void MainWindow::openSettings(int index, const QString ¶m) { - main_layout->setCurrentWidget(settingsWindow); - settingsWindow->setCurrentPanel(index, param); -} - -void MainWindow::closeSettings() { - main_layout->setCurrentWidget(homeWindow); - - if (uiState()->scene.started) { - homeWindow->showSidebar(false); - } -} - -bool MainWindow::eventFilter(QObject *obj, QEvent *event) { - bool ignore = false; - switch (event->type()) { - case QEvent::TouchBegin: - case QEvent::TouchUpdate: - case QEvent::TouchEnd: - case QEvent::MouseButtonPress: - case QEvent::MouseMove: { - // ignore events when device is awakened by resetInteractiveTimeout - ignore = !device()->isAwake(); - device()->resetInteractiveTimeout(); - -#ifdef SUNNYPILOT - auto *s_sp = uiStateSP(); - bool onroadScreenControl = s_sp->scene.onroadScreenOffControl; - bool started = s_sp->scene.started; - bool timerExpired = (s_sp->scene.onroadScreenOffTimer == 0); - ignore |= (onroadScreenControl and started and timerExpired); - s_sp->reset_onroad_sleep_timer(); -#endif - - break; - } - default: - break; - } - return ignore; -} diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h deleted file mode 100644 index 63293d4177..0000000000 --- a/selfdrive/ui/qt/window.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include - -#include "selfdrive/ui/qt/home.h" -#include "selfdrive/ui/qt/offroad/onboarding.h" -#include "selfdrive/ui/qt/offroad/settings.h" - -#ifdef SUNNYPILOT -#include "selfdrive/ui/sunnypilot/ui.h" -#endif - -class MainWindow : public QWidget { - Q_OBJECT - -public: - 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 = ""); - - QStackedLayout *main_layout; - OnboardingWindow *onboardingWindow; -}; diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index 01c7c68949..481b7a12d5 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -8,12 +8,12 @@ from cereal import car, messaging, custom from openpilot.common.basedir import BASEDIR from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import Ratekeeper -from openpilot.common.retry import retry +from openpilot.common.utils import retry from openpilot.common.swaglog import cloudlog from openpilot.system import micd -from openpilot.selfdrive.ui.sunnypilot.quiet_mode import QuietMode +from openpilot.sunnypilot.selfdrive.ui.quiet_mode import QuietMode SAMPLE_RATE = 48000 SAMPLE_BUFFER = 4096 # (approx 100ms) @@ -137,7 +137,7 @@ class Soundd(QuietMode): volume = ((weighted_db - AMBIENT_DB) / DB_SCALE) * (MAX_VOLUME - MIN_VOLUME) + MIN_VOLUME return math.pow(10, (np.clip(volume, MIN_VOLUME, MAX_VOLUME) - 1)) - @retry(attempts=7, delay=3) + @retry(attempts=10, delay=3) def get_stream(self, sd): # reload sounddevice to reinitialize portaudio sd._terminate() diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript deleted file mode 100644 index 449093886a..0000000000 --- a/selfdrive/ui/sunnypilot/SConscript +++ /dev/null @@ -1,107 +0,0 @@ -widgets_src = [ - "sunnypilot/qt/request_repeater.cc", - "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/external_storage.cc", - "sunnypilot/qt/widgets/prime.cc", - "sunnypilot/qt/widgets/scrollview.cc", - "sunnypilot/qt/network/networking.cc", -] - -qt_util = [ - "sunnypilot/qt/util.cc", -] - -qt_src = [ - "sunnypilot/ui.cc", - "sunnypilot/qt/api.cc", - "sunnypilot/qt/sidebar.cc", - "sunnypilot/qt/window.cc", - "sunnypilot/qt/home.cc", - "sunnypilot/qt/offroad/exit_offroad_button.cc", - "sunnypilot/qt/offroad/offroad_home.cc", - "sunnypilot/qt/offroad/settings/developer_panel.cc", - "sunnypilot/qt/offroad/settings/device_panel.cc", - "sunnypilot/qt/offroad/settings/display_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/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", - "sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc", - "sunnypilot/qt/offroad/settings/sunnylink/community_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/alerts.cc", - "sunnypilot/qt/onroad/annotated_camera.cc", - "sunnypilot/qt/onroad/buttons.cc", - "sunnypilot/qt/onroad/developer_ui/developer_ui.cc", - "sunnypilot/qt/onroad/hud.cc", - "sunnypilot/qt/onroad/model.cc", - "sunnypilot/qt/onroad/onroad_home.cc", -] - -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", - "sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.cc", - "sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.cc", -] - -longitudinal_panel_qt_src = [ - "sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc", - "sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.cc", - "sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_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", -] - -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", - "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", -] - -display_panel_qt_src = [ - "sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.cc", -] - -sp_widgets_src = widgets_src + network_src -sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + \ - longitudinal_panel_qt_src + osm_panel_qt_src + display_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/api.cc b/selfdrive/ui/sunnypilot/qt/api.cc deleted file mode 100644 index d75d8d39f0..0000000000 --- a/selfdrive/ui/sunnypilot/qt/api.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/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 deleted file mode 100644 index 592b2e668f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/api.h +++ /dev/null @@ -1,36 +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/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/common/json_fetcher.h b/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h deleted file mode 100644 index 5691f600a6..0000000000 --- a/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h +++ /dev/null @@ -1,41 +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 - -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/home.cc b/selfdrive/ui/sunnypilot/qt/home.cc deleted file mode 100644 index 92b6e724fe..0000000000 --- a/selfdrive/ui/sunnypilot/qt/home.cc +++ /dev/null @@ -1,21 +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/home.h" - -#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.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 deleted file mode 100644 index 4cf532d14c..0000000000 --- a/selfdrive/ui/sunnypilot/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 "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/network/networking.cc b/selfdrive/ui/sunnypilot/qt/network/networking.cc deleted file mode 100644 index 3db65a5b51..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/networking.cc +++ /dev/null @@ -1,45 +0,0 @@ -#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 deleted file mode 100644 index f220b8a50a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/networking.h +++ /dev/null @@ -1,17 +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/network/networking.h" - - -class NetworkingSP : public Networking { - Q_OBJECT - -public: - explicit NetworkingSP(QWidget *parent = nullptr); -}; diff --git a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h b/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h deleted file mode 100644 index 04b4dccf14..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h +++ /dev/null @@ -1,56 +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 - -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 deleted file mode 100644 index 7e1772b591..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h +++ /dev/null @@ -1,83 +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 - -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 deleted file mode 100644 index 4d255ac8e3..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h +++ /dev/null @@ -1,37 +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 - -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 deleted file mode 100644 index 3919a1821f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.cc +++ /dev/null @@ -1,57 +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/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 deleted file mode 100644 index 8ef99d1013..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h +++ /dev/null @@ -1,31 +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/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 deleted file mode 100644 index bffe32839f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.cc +++ /dev/null @@ -1,36 +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/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 deleted file mode 100644 index abd0e1ab83..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.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 "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 deleted file mode 100644 index cd16360c26..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.cc +++ /dev/null @@ -1,42 +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/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 deleted file mode 100644 index 90eb354751..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.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 "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 deleted file mode 100644 index 4428eb55c4..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.cc +++ /dev/null @@ -1,14 +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/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 deleted file mode 100644 index 0961449926..0000000000 --- a/selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h +++ /dev/null @@ -1,22 +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/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/exit_offroad_button.cc b/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.cc deleted file mode 100644 index 0c7902476a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.cc +++ /dev/null @@ -1,100 +0,0 @@ -#include -#include -#include -#include -#include - -#include "selfdrive/ui/ui.h" -#include "selfdrive/ui/qt/widgets/input.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h" - - -ExitOffroadButton::ExitOffroadButton(QWidget *parent) : QPushButton(parent), glowTimer(new QTimer(this)) { - setMouseTracking(true); - - connect(glowTimer, &QTimer::timeout, this, [this]() { - // Pulse alpha up and down - glowAlpha += glowDelta; - if (glowAlpha > 220 || glowAlpha < 10) { - glowDelta *= -1; - } - update(); // trigger repaint - }); - - glowTimer->start(45); - - pixmap = QPixmap("../../sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png").scaledToWidth(img_width, Qt::SmoothTransformation); - - // go to toggles and expand experimental mode description - connect(this, &QPushButton::clicked, [=]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to exit Always Offroad mode?"), tr("Confirm"), this)) { - params.remove("OffroadMode"); - } - }); - - setFixedHeight(125); - QHBoxLayout *main_layout = new QHBoxLayout; - main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0); - - mode_label = new QLabel(tr("EXIT ALWAYS OFFROAD MODE")); - mode_icon = new QLabel; - mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); - mode_icon->setPixmap(pixmap); - - main_layout->addWidget(mode_label, 1, Qt::AlignLeft); - main_layout->addWidget(mode_icon, 0, Qt::AlignRight); - - setLayout(main_layout); - - setStyleSheet(R"( - QPushButton { - border: none; - } - - QLabel { - font-size: 45px; - font-weight: 300; - text-align: left; - font-family: JetBrainsMono; - color: #000000; - } - )"); -} - -void drawPulsingGlowOverlay(QPainter &p, QPainterPath path, int glowAlpha) { - // Draw pulsing glow effect clipped to button area - p.save(); - p.setClipPath(path); - p.setCompositionMode(QPainter::CompositionMode_HardLight); - - const QColor animatedGlowColor(255, 255, 255, std::min(255, glowAlpha)); - QPen glowPen(animatedGlowColor, 8); - glowPen.setJoinStyle(Qt::RoundJoin); - p.setPen(glowPen); - p.drawPath(path); - - p.restore(); -} - -void ExitOffroadButton::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QPainterPath path; - path.addRoundedRect(rect(), 10, 10); - - // gradient - bool pressed = isDown(); - QLinearGradient gradient(rect().left(), 0, rect().right(), 0); - gradient.setColorAt(0, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); - gradient.setColorAt(0.3, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); - gradient.setColorAt(1, QColor(20, 255, 171, pressed ? 0xcc : 0xff)); - p.fillPath(path, gradient); - - drawPulsingGlowOverlay(p, path, glowAlpha); - - // vertical line - p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine)); - int line_x = rect().right() - img_width - (2 * horizontal_padding); - p.drawLine(line_x, rect().bottom(), line_x, rect().top()); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h b/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h deleted file mode 100644 index 673d9fff73..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -#include "common/params.h" - -class ExitOffroadButton : public QPushButton { - Q_OBJECT - -private: - QTimer *glowTimer; - int glowAlpha = 100; // Current alpha of glow - int glowDelta = 10; // Change per tick - -public: - explicit ExitOffroadButton(QWidget* parent = 0); - - Params params; - bool offroad_mode; - int img_width = 100; - int horizontal_padding = 30; - QPixmap pixmap; - QLabel *mode_label; - QLabel *mode_icon; - -protected: - void paintEvent(QPaintEvent *event) override; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc deleted file mode 100644 index cc82e6ddb9..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc +++ /dev/null @@ -1,49 +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 - -#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h" - -OffroadHomeSP::OffroadHomeSP(QWidget *parent) : OffroadHome(parent) { - QFrame *left_widget = new QFrame(this); - QVBoxLayout *left_layout = new QVBoxLayout(left_widget); - left_layout->setContentsMargins(0, 0, 0, 0); - left_layout->setSpacing(30); - - btn_exit_offroad = new ExitOffroadButton(this); - QObject::connect(btn_exit_offroad, &ExitOffroadButton::clicked, [=]() { - refreshOffroadStatus(); - }); - left_layout->addWidget(btn_exit_offroad); - - left_layout->addWidget(new DriveStats(this)); - left_widget->setStyleSheet("border-radius: 10px;"); - - home_layout->insertWidget(0, left_widget); - - offroad_notif = new QPushButton(tr("ALWAYS OFFROAD ACTIVE")); - offroad_notif->setVisible(false); - offroad_notif->setStyleSheet("background-color: #E22C2C;"); - header_layout->insertWidget(0, offroad_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); - - QObject::connect(deviceSP(), &DeviceSP::displayPowerChanged, this, &OffroadHomeSP::refreshOffroadStatus); - -} - -void OffroadHomeSP::showEvent(QShowEvent *event) { - refreshOffroadStatus(); - OffroadHome::showEvent(event); -} - -void OffroadHomeSP::refreshOffroadStatus() { - bool is_offroad = params.getBool("OffroadMode"); - btn_exit_offroad->setVisible(is_offroad); - offroad_notif->setVisible(is_offroad); - OffroadHome::refresh(); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h deleted file mode 100644 index 30816f666c..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.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/offroad/offroad_home.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h" - -class OffroadHomeSP : public OffroadHome { - Q_OBJECT - -public: - explicit OffroadHomeSP(QWidget *parent = 0); - -private: - ExitOffroadButton *btn_exit_offroad; - QPushButton *offroad_notif; - Params params; - - void showEvent(QShowEvent *event) override; - void refreshOffroadStatus(); -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc deleted file mode 100644 index e9bd13485a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc +++ /dev/null @@ -1,50 +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/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("Global Brightness"), - tr("Overrides the brightness of the device. This applies to both onroad and offroad screens. "), - "../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 deleted file mode 100644 index 20d0a291d9..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h +++ /dev/null @@ -1,25 +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/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/developer_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc deleted file mode 100644 index 9eb8da71ec..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc +++ /dev/null @@ -1,91 +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/developer_panel.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h" - -DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(parent) { - - #ifndef __APPLE__ - addItem(new ExternalStorageControl()); - #endif - - // Advanced Controls Toggle - showAdvancedControls = new ParamControlSP("ShowAdvancedControls", tr("Show Advanced Controls"), tr("Toggle visibility of advanced sunnypilot controls.\nThis only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state."), ""); - addItem(showAdvancedControls); - - QObject::connect(showAdvancedControls, &ParamControlSP::toggleFlipped, this, [=](bool) { - AbstractControlSP::UpdateAllAdvancedControls(); - updateToggles(!uiState()->scene.started); - }); - showAdvancedControls->showDescription(); - - // Github Runner Toggle - enableGithubRunner = new ParamControlSP("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), "", this, true); - addItem(enableGithubRunner); - - // Copyparty Toggle - enableCopyparty = new ParamControlSP("EnableCopyparty", tr("Enable Copyparty service"), tr("Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP."), "", this, false); - addItem(enableCopyparty); - - // Quickboot Mode Toggle - prebuiltToggle = new ParamControlSP("QuickBootToggle", tr("Enable Quickboot Mode"), tr(""), "", this, true); - addItem(prebuiltToggle); - - QObject::connect(prebuiltToggle, &ParamControl::toggleFlipped, [=](bool state) { - QString prebuiltPath = "/data/openpilot/prebuilt"; - state ? QFile(prebuiltPath).open(QIODevice::WriteOnly) : QFile::remove(prebuiltPath); - prebuiltToggle->refresh(); - }); - prebuiltToggle->setVisible(false); - - // Error log button - errorLogBtn = new ButtonControlSP(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes.")); - connect(errorLogBtn, &ButtonControlSP::clicked, [=]() { - QFileInfo file("/data/community/crashes/error.log"); - QString text; - if (file.exists()) { - text = "" + file.lastModified().toString("dd-MMM-yyyy hh:mm:ss ").toUpper() + "

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

To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.") - : tr("Quickboot mode requires updates to be disabled.
Enable 'Disable Updates' in the Software panel first.")); - prebuiltToggle->showDescription(); - - enableGithubRunner->setVisible(!is_release); - errorLogBtn->setVisible(!is_release); - showAdvancedControls->setEnabled(true); - - joystickToggle->setVisible(!is_release); - longManeuverToggle->setVisible(!is_release); -} - -void DeveloperPanelSP::showEvent(QShowEvent *event) { - DeveloperPanel::showEvent(event); - AbstractControlSP::UpdateAllAdvancedControls(); - updateToggles(!uiState()->scene.started); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h deleted file mode 100644 index ed64964a56..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h +++ /dev/null @@ -1,31 +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/developer_panel.h" - -class DeveloperPanelSP : public DeveloperPanel { - Q_OBJECT -public: - explicit DeveloperPanelSP(SettingsWindow *parent); - -private: - ParamControlSP *enableCopyparty; - ParamControlSP *enableGithubRunner; - ButtonControlSP *errorLogBtn; - ParamControlSP *prebuiltToggle; - Params params; - ParamControlSP *showAdvancedControls; - -private slots: - void updateToggles(bool offroad); - -protected: - void showEvent(QShowEvent *event) override; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc deleted file mode 100644 index 8295789f0a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ /dev/null @@ -1,203 +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/device_panel.h" - -#include "common/watchdog.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 = { - {"quietModeBtn", tr("Quiet Mode"), "QuietMode"}, - {"dcamBtn", tr("Driver Camera Preview"), ""}, - {"retrainingBtn", tr("Training Guide"), ""}, - {"regulatoryBtn", tr("Regulatory"), ""}, - {"translateBtn", tr("Language"), ""}, - {"resetParams", tr("Reset Settings"), ""}, - {"onroadUploadsBtn", tr("Onroad Uploads"), "OnroadUploads"} - }; - - int row = 0, col = 0; - for (const auto &[id, text, param] : device_btns) { - if (id == "regulatoryBtn" && !Hardware::TICI()) { - continue; - } - - auto *btn = new PushButtonSP(text, 750, this, param); - btn->setObjectName(id); - buttons[id] = btn; - - 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++; - } - } - - 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(); - } - }); - - 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); - }); - } - - 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); - } - }); - - connect(buttons["resetParams"], &PushButtonSP::clicked, this, &DevicePanelSP::resetSettings); - - connect(buttons["onroadUploadsBtn"], &PushButtonSP::clicked, buttons["onroadUploadsBtn"], &PushButtonSP::updateButton); - - // Max Time Offroad - maxTimeOffroad = new MaxTimeOffroad(); - connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh); - addItem(maxTimeOffroad); - - toggleDeviceBootMode = new ButtonParamControlSP("DeviceBootMode", tr("Wake-Up Behavior"), "", "", {"Default", "Offroad"}, 375, true); - addItem(toggleDeviceBootMode); - - connect(toggleDeviceBootMode, &ButtonParamControlSP::buttonClicked, this, [=](int index) { - params.put("DeviceBootMode", QString::number(index).toStdString()); - updateState(offroad); - }); - - addItem(device_grid_layout); - - // offroad mode and power buttons - - QHBoxLayout *power_layout = new QHBoxLayout(); - power_layout->setSpacing(25); - - 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"), 750, 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); - } - - offroadBtn = new PushButtonSP(tr("Offroad Mode")); - offroadBtn->setFixedWidth(power_layout->sizeHint().width()); - QObject::connect(offroadBtn, &PushButtonSP::clicked, this, &DevicePanelSP::setOffroadMode); - - power_group_layout = new QVBoxLayout(); - power_group_layout->setSpacing(25); - power_group_layout->addLayout(power_layout); - - addItem(power_group_layout); - - always_enabled_btns = { - rebootBtn, - poweroffBtn, - offroadBtn, - buttons["quietModeBtn"], - buttons["onroadUploadsBtn"], - }; - - QObject::connect(uiState(), &UIState::offroadTransition, this, &DevicePanelSP::updateState); -} - -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(offroad); -} - -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(offroad); -} - -void DevicePanelSP::updateState(bool _offroad) { - for (auto btn : findChildren()) { - bool always_enabled = std::find(always_enabled_btns.begin(), always_enabled_btns.end(), btn) != always_enabled_btns.end(); - - if (!always_enabled) { - btn->setEnabled(_offroad); - } - } - - bool offroad_mode_param = params.getBool("OffroadMode"); - offroadBtn->setText(offroad_mode_param ? tr("Exit Always Offroad") : tr("Enable Always Offroad")); - offroadBtn->setStyleSheet(offroad_mode_param ? alwaysOffroadStyle : autoOffroadStyle); - - DeviceSleepModeStatus currStatus = DeviceSleepModeStatus::DEFAULT; - if (params.get("DeviceBootMode") == "1") { - currStatus = DeviceSleepModeStatus::OFFROAD; - } - toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus)); - - if (offroad and not offroad_mode_param) { - power_group_layout->insertWidget(0, offroadBtn, 0, Qt::AlignHCenter); - } else { - AddWidgetAt(0, offroadBtn); - } - - offroad = _offroad; -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h deleted file mode 100644 index 425c4528a1..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ /dev/null @@ -1,114 +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/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" - -enum class DeviceSleepModeStatus { - DEFAULT, - OFFROAD, -}; - -class DevicePanelSP : public DevicePanel { - Q_OBJECT - -public: - explicit DevicePanelSP(SettingsWindowSP *parent = 0); - void showEvent(QShowEvent *event) override; - void setOffroadMode(); - void updateState(bool _offroad); - void resetSettings(); - -private: - std::map buttons; - PushButtonSP *offroadBtn; - MaxTimeOffroad *maxTimeOffroad; - ButtonParamControlSP *toggleDeviceBootMode; - QVBoxLayout *power_group_layout; - bool offroad; - - std::vector always_enabled_btns = {}; - - 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 { - 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; - } - )"; - - static QString deviceSleepModeDescription(DeviceSleepModeStatus status = DeviceSleepModeStatus::DEFAULT) { - QString def_str = tr("⁍ Default: Device will boot/wake-up normally & will be ready to engage."); - QString offrd_str = tr("⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up."); - - if (status == DeviceSleepModeStatus::DEFAULT) { - def_str = "" + def_str + ""; - } else if (status == DeviceSleepModeStatus::OFFROAD) { - offrd_str = "" + offrd_str + ""; - } - - return QString("%1

%2
%3") - .arg(tr("Controls state of the device after boot/sleep.")) - .arg(def_str) - .arg(offrd_str); - } -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.cc deleted file mode 100644 index e29d06cb82..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.cc +++ /dev/null @@ -1,54 +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/display/onroad_screen_brightness.h" - -OnroadScreenBrightnessControl::OnroadScreenBrightnessControl(const QString ¶m, const QString &title, - const QString &description, const QString &icon, - QWidget *parent) - : ExpandableToggleRow(param, title, description, icon, parent) { - auto *mainFrame = new QFrame(this); - auto *mainFrameLayout = new QVBoxLayout(); - mainFrame->setLayout(mainFrameLayout); - mainFrameLayout->setSpacing(30); - mainFrameLayout->setContentsMargins(0, 0, 0, 0); - - onroadScreenOffTimer = new OptionControlSP( - "OnroadScreenOffTimer", - "Onroad Brightness Delay", - "", - "", - {0, 11}, 1, true, &onroadScreenOffTimerOptions); - - onroadScreenBrightness = new OptionControlSP( - "OnroadScreenOffBrightness", - "Onroad Brightness", - "", - "", - {0, 90}, 10, true); - - connect(onroadScreenOffTimer, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh); - connect(onroadScreenBrightness, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh); - mainFrameLayout->addWidget(onroadScreenBrightness); - mainFrameLayout->addWidget(onroadScreenOffTimer); - - addItem(mainFrame); - - refresh(); -} - -void OnroadScreenBrightnessControl::refresh() { - // Driving Screen Off Timer - int valTimer = std::atoi(params.get("OnroadScreenOffTimer").c_str()); - std::string labelTimer = (valTimer < 60 ? std::to_string(valTimer) + "s" : std::to_string(valTimer / 60) + "m"); - onroadScreenOffTimer->setLabel(QString::fromStdString(labelTimer)); - - // Driving Screen Off Brightness - std::string valBrightness = params.get("OnroadScreenOffBrightness"); - std::string labelBrightness = (valBrightness == "0" ? " Screen Off" : valBrightness + "%"); - onroadScreenBrightness->setLabel(QString::fromStdString(labelBrightness)); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.h deleted file mode 100644 index 02335d1ce6..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.h +++ /dev/null @@ -1,42 +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/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" - -static const QMap onroadScreenOffTimerOptions = { - {"0", "15"}, - {"1", "30"}, - {"2", "60"}, - {"3", "120"}, - {"4", "180"}, - {"5", "240"}, - {"6", "300"}, - {"7", "360"}, - {"8", "420"}, - {"9", "480"}, - {"10", "540"}, - {"11", "600"} -}; - -class OnroadScreenBrightnessControl : public ExpandableToggleRow { - Q_OBJECT - -public: - OnroadScreenBrightnessControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - QWidget *parent = nullptr); - void refresh(); - -private: - Params params; - OptionControlSP *onroadScreenOffTimer; - OptionControlSP *onroadScreenBrightness; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.cc deleted file mode 100644 index 465047f178..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.cc +++ /dev/null @@ -1,65 +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/display_panel.h" - -DisplayPanel::DisplayPanel(QWidget *parent) : QWidget(parent) { - 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); - - // Onroad Screen Off/Brightness - onroadScreenBrightnessControl = new OnroadScreenBrightnessControl( - "OnroadScreenOffControl", - tr("Onroad Screen: Reduced Brightness"), - tr("Turn off device screen or reduce brightness after driving starts. " - "It automatically brightens again when screen is touched or a visible alert is displayed."), - "", - this); - list->addItem(onroadScreenBrightnessControl); - list->addItem(horizontal_line()); - - // Global Brightness - brightness = new Brightness(); - connect(brightness, &OptionControlSP::updateLabels, brightness, &Brightness::refresh); - list->addItem(brightness); - list->addItem(horizontal_line()); - - // Interactivity Timeout - interactivityTimeout = new OptionControlSP("InteractivityTimeout", tr("Interactivity Timeout"), - tr("Apply a custom timeout for settings UI." - "\nThis is the time after which settings UI closes automatically if user is not interacting with the screen."), - "", {0, 120}, 10, true, nullptr, false); - - connect(interactivityTimeout, &OptionControlSP::updateLabels, [=]() { - refresh(); - }); - list->addItem(interactivityTimeout); - - sunnypilotScroller = new ScrollViewSP(list, this); - vlayout->addWidget(sunnypilotScroller); - main_layout->addWidget(sunnypilotScreen); -} - -void DisplayPanel::showEvent(QShowEvent *event) { - QWidget::showEvent(event); - refresh(); -} - -void DisplayPanel::refresh() { - onroadScreenBrightnessControl->refresh(); - - QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout")); - if (timeoutValue == "0" || timeoutValue.isEmpty()) { - interactivityTimeout->setLabel("Default"); - } else { - interactivityTimeout->setLabel(timeoutValue + "s"); - } -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.h deleted file mode 100644 index 69b440c9d3..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/display_panel.h +++ /dev/null @@ -1,31 +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/sunnypilot/qt/offroad/settings/brightness.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -class DisplayPanel : public QWidget { - Q_OBJECT - -public: - explicit DisplayPanel(QWidget *parent = nullptr); - void showEvent(QShowEvent *event) override; - void refresh(); - -private: - QStackedLayout* main_layout = nullptr; - QWidget* sunnypilotScreen = nullptr; - ScrollViewSP *sunnypilotScroller = nullptr; - Params params; - OnroadScreenBrightnessControl *onroadScreenBrightnessControl = nullptr; - Brightness *brightness; - OptionControlSP *interactivityTimeout; -}; 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 deleted file mode 100644 index b43c6162c6..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.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 "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 deleted file mode 100644 index 7f02337b9c..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h +++ /dev/null @@ -1,25 +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/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/lane_change_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc deleted file mode 100644 index 53717508bf..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc +++ /dev/null @@ -1,111 +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. - * - * 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 deleted file mode 100644 index 5bd458c47d..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h +++ /dev/null @@ -1,51 +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/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/mads_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc deleted file mode 100644 index 749bc9e3f3..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.cc +++ /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. - */ - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/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"), "", ""); - list->addItem(madsMainCruiseToggle); - - // Unified Engagement Mode - madsUnifiedEngagementModeToggle = new ParamControl("MadsUnifiedEngagementMode", tr("Unified Engagement Mode (UEM)"), "", ""); - list->addItem(madsUnifiedEngagementModeToggle); - - // Steering Mode On Brake - madsSteeringMode = new ButtonParamControl("MadsSteeringMode", tr("Steering Mode on Brake Pedal"), "", "", madsSteeringModeTexts(), 500); - QObject::connect(madsSteeringMode, &ButtonParamControl::buttonClicked, [=] { - updateToggles(offroad); - }); - list->addItem(madsSteeringMode); - - 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) { - 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)) - ); - - 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 (madsLimitedSettings(CP, CP_SP)) { - 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 deleted file mode 100644 index 9cf62a0ffe..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h +++ /dev/null @@ -1,124 +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" - -inline bool madsLimitedSettings(const cereal::CarParams::Reader &CP, const cereal::CarParamsSP::Reader &CP_SP) { - if (CP.getBrand() == "rivian") { - return true; - } - if (CP.getBrand() == "tesla") { - return !(CP_SP.getFlags() & 1); // 1 == TeslaFlagsSP.HAS_VEHICLE_BUS - } - - return false; -} - -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 - -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 *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.")}, - }; - 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()) { - texts.push_back(option.display_text); - } - return texts; - } - - 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 + "

"; - - for (const auto& option : madsSteeringModeOptions()) { - QString desc = option.description; - if (option.mode == mode) { - desc = "" + desc + ""; - } - result += desc + "
"; - } - - 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/neural_network_lateral_control.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc deleted file mode 100644 index 02ab810c57..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc +++ /dev/null @@ -1,67 +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(offroad); -} - -void NeuralNetworkLateralControl::updateToggle(bool _offroad) { - QString statusInitText = "" + STATUS_CHECK_COMPATIBILITY + ""; - QString notLoadedText = "" + STATUS_NOT_LOADED + ""; - QString loadedText = "" + STATUS_LOADED + ""; - - bool allowed = true; - 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)); - allowed = 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(); - } - - bool enforce_torque_toggle = params.getBool("EnforceTorqueControl"); - setEnabled(_offroad && allowed && !enforce_torque_toggle); - - refresh(); - - offroad = _offroad; -} 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 9e872b11e9..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h +++ /dev/null @@ -1,62 +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(); - -public slots: - void updateToggle(bool _offroad); - -private: - Params params; - bool offroad; - - // 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/torque_lateral_control_custom_params.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.cc deleted file mode 100644 index 894fbc2ec4..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.cc +++ /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. - */ - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.h" - -TorqueLateralControlCustomParams::TorqueLateralControlCustomParams(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent) - : ExpandableToggleRow(param, title, description, icon, parent) { - - QFrame *frame = new QFrame(this); - QGridLayout *frame_layout = new QGridLayout(); - frame->setLayout(frame_layout); - frame_layout->setSpacing(0); - - torqueLateralControlParamsOverride = new ParamControl( - "TorqueParamsOverrideEnabled", - tr("Manual Real-Time Tuning"), - tr("Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values."), - "../assets/offroad/icon_blank.png", - this - ); - connect(torqueLateralControlParamsOverride, &ParamControl::toggleFlipped, this, &TorqueLateralControlCustomParams::refresh); - - torqueParamsOverrideLatAccelFactor = new OptionControlSP("TorqueParamsOverrideLatAccelFactor", tr("Lateral Acceleration Factor"), "", "", {1, 500}, 1, false, nullptr, true, false); - connect(torqueParamsOverrideLatAccelFactor, &OptionControlSP::updateLabels, this, &TorqueLateralControlCustomParams::refresh); - torqueParamsOverrideLatAccelFactor->setFixedWidth(280); - - torqueParamsOverrideFriction = new OptionControlSP("TorqueParamsOverrideFriction", tr("Friction"), "", "", {1, 100}, 1, false, nullptr, true, false); - connect(torqueParamsOverrideFriction, &OptionControlSP::updateLabels, this, &TorqueLateralControlCustomParams::refresh); - torqueParamsOverrideFriction->setFixedWidth(280); - - frame_layout->addWidget(torqueLateralControlParamsOverride, 0, 0, 1, 2); - QSpacerItem *spacer = new QSpacerItem(20, 40); - frame_layout->addItem(spacer, 1, 0, 1, 2); - frame_layout->addWidget(torqueParamsOverrideLatAccelFactor, 2, 0, Qt::AlignCenter); - frame_layout->addWidget(torqueParamsOverrideFriction, 2, 1, Qt::AlignCenter); - - addItem(frame); - - refresh(); -} - -void TorqueLateralControlCustomParams::refresh() { - bool torque_override_param = params.getBool("TorqueParamsOverrideEnabled"); - float laf_param = QString::fromStdString(params.get("TorqueParamsOverrideLatAccelFactor")).toFloat(); - const QString laf_unit = "m/s²"; - - float friction_param = QString::fromStdString(params.get("TorqueParamsOverrideFriction")).toFloat(); - - torqueParamsOverrideLatAccelFactor->setTitle(tr("Lateral Acceleration Factor") + "\n(" + (torque_override_param ? tr("Real-time and Offline") : tr("Offline Only")) + ")"); - torqueParamsOverrideFriction->setTitle(tr("Friction") + "\n(" + (torque_override_param ? tr("Real-time and Offline") : tr("Offline Only")) + ")"); - - torqueParamsOverrideLatAccelFactor->setLabel(QString::number(laf_param, 'f', 2) + " " + laf_unit); - torqueParamsOverrideFriction->setLabel(QString::number(friction_param, 'f', 2)); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.h deleted file mode 100644 index 8442bd0d48..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.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 "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 TorqueLateralControlCustomParams : public ExpandableToggleRow { - Q_OBJECT - -public: - TorqueLateralControlCustomParams(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent = nullptr); - void refresh(); - -private: - Params params; - ParamControl *torqueLateralControlParamsOverride; - OptionControlSP *torqueParamsOverrideFriction; - OptionControlSP *torqueParamsOverrideLatAccelFactor; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.cc deleted file mode 100644 index 914ce7a939..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.cc +++ /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. - */ - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.h" - -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -TorqueLateralControlSettings::TorqueLateralControlSettings(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); - // param, title, desc, icon - std::vector> toggle_defs{ - { - "LiveTorqueParamsToggle", - tr("Self-Tune"), - tr("Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default."), - "../assets/offroad/icon_blank.png", - }, - { - "LiveTorqueParamsRelaxedToggle", - tr("Less Restrict Settings for Self-Tune (Beta)"), - tr("Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values."), - "../assets/offroad/icon_blank.png", - } - }; - - for (auto &[param, title, desc, icon] : toggle_defs) { - auto toggle = new ParamControlSP(param, title, desc, icon, this); - list->addItem(toggle); - toggles[param.toStdString()] = toggle; - } - - torqueLateralControlCustomParams = new TorqueLateralControlCustomParams( - "CustomTorqueParams", - tr("Enable Custom Tuning"), - tr("Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within \"opendbc/car/torque_data\". " - "The values will also be used live when \"Manual Real-Time Tuning\" toggle is enabled."), - "../assets/offroad/icon_blank.png", - this); - list->addItem(torqueLateralControlCustomParams); - - QObject::connect(uiState(), &UIState::offroadTransition, this, &TorqueLateralControlSettings::updateToggles); - QObject::connect(toggles["LiveTorqueParamsToggle"], &ParamControlSP::toggleFlipped, [=](bool state) { - if (!state) { - params.remove("LiveTorqueParamsRelaxedToggle"); - toggles["LiveTorqueParamsRelaxedToggle"]->refresh(); - } - - updateToggles(offroad); - }); - - main_layout->addWidget(new ScrollViewSP(list, this)); -} - -void TorqueLateralControlSettings::showEvent(QShowEvent *event) { - updateToggles(offroad); -} - -void TorqueLateralControlSettings::updateToggles(bool _offroad) { - bool live_toggle = toggles["LiveTorqueParamsToggle"]->isToggled(); - - toggles["LiveTorqueParamsToggle"]->setEnabled(_offroad); - toggles["LiveTorqueParamsRelaxedToggle"]->setEnabled(_offroad && live_toggle); - - torqueLateralControlCustomParams->setEnabled(_offroad); - torqueLateralControlCustomParams->refresh(); - - offroad = _offroad; -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.h deleted file mode 100644 index 6254073dfe..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.h +++ /dev/null @@ -1,36 +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/lateral/torque_lateral_control_custom_params.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" - -class TorqueLateralControlSettings : public QWidget { - Q_OBJECT - -public: - explicit TorqueLateralControlSettings(QWidget *parent = nullptr); - - void showEvent(QShowEvent *event) override; - - signals: - void backPress(); - -public slots: - void updateToggles(bool _offroad); - -private: - Params params; - bool offroad; - std::map toggles; - - TorqueLateralControlCustomParams *torqueLateralControlCustomParams; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc deleted file mode 100644 index ac004e8856..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ /dev/null @@ -1,198 +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_panel.h" - -#include "common/util.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" - -LateralPanel::LateralPanel(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 sunnypilot engagement/disengagement."), - ""); - madsToggle->setConfirmation(true, false); - list->addItem(madsToggle); - - 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, &PushButtonSP::setEnabled); - - madsWidget = new MadsSettings(this); - connect(madsWidget, &MadsSettings::backPress, [=]() { - sunnypilotScroller->restoreScrollPosition(); - main_layout->setCurrentWidget(sunnypilotScreen); - }); - 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()); - - // 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()); - - // Customized Torque Lateral Control - torqueLateralControlToggle = new ParamControl( - "EnforceTorqueControl", - tr("Enforce Torque Lateral Control"), - tr("Enable this to enforce sunnypilot to steer with Torque lateral control."), - ""); - list->addItem(torqueLateralControlToggle); - - torqueLateralControlSettingsButton = new PushButtonSP(tr("Customize Params")); - torqueLateralControlSettingsButton->setObjectName("torque_btn"); - connect(torqueLateralControlSettingsButton, &QPushButton::clicked, [=]() { - sunnypilotScroller->setLastScrollPosition(); - main_layout->setCurrentWidget(torqueLateralControlWidget); - }); - QObject::connect(torqueLateralControlToggle, &ToggleControl::toggleFlipped, [=](bool state) { - torqueLateralControlSettingsButton->setEnabled(state); - nnlcToggle->updateToggle(offroad); - updateToggles(offroad); - }); - - torqueLateralControlWidget = new TorqueLateralControlSettings(this); - connect(torqueLateralControlWidget, &TorqueLateralControlSettings::backPress, [=]() { - sunnypilotScroller->restoreScrollPosition(); - main_layout->setCurrentWidget(sunnypilotScreen); - }); - list->addItem(torqueLateralControlSettingsButton); - - list->addItem(vertical_space(0)); - list->addItem(horizontal_line()); - - // Neural Network Lateral Control - nnlcToggle = new NeuralNetworkLateralControl(); - list->addItem(nnlcToggle); - - QObject::connect(nnlcToggle, &ParamControl::toggleFlipped, [=](bool state) { - if (state) { - nnlcToggle->showDescription(); - } else { - nnlcToggle->hideDescription(); - } - - nnlcToggle->updateToggle(offroad); - updateToggles(offroad); - }); - - QObject::connect(uiState(), &UIState::offroadTransition, this, &LateralPanel::updateToggles); - - sunnypilotScroller = new ScrollViewSP(list, this); - vlayout->addWidget(sunnypilotScroller); - - main_layout->addWidget(sunnypilotScreen); - main_layout->addWidget(madsWidget); - main_layout->addWidget(laneChangeWidget); - main_layout->addWidget(torqueLateralControlWidget); - - 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 LateralPanel::showEvent(QShowEvent *event) { - nnlcToggle->updateToggle(offroad); - updateToggles(offroad); -} - -void LateralPanel::hideEvent(QHideEvent *event) { - main_layout->setCurrentWidget(sunnypilotScreen); -} - -void LateralPanel::updateToggles(bool _offroad) { - bool torque_allowed = true; - 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 (madsLimitedSettings(CP, CP_SP)) { - madsToggle->setDescription(descriptionBuilder(STATUS_MADS_SETTINGS_LIMITED_COMPATIBILITY, MADS_BASE_DESC)); - } else { - madsToggle->setDescription(descriptionBuilder(STATUS_MADS_SETTINGS_FULL_COMPATIBILITY, MADS_BASE_DESC)); - } - - if (CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE) { - params.remove("EnforceTorqueControl"); - torque_allowed = false; - } - } else { - madsToggle->setDescription(descriptionBuilder(STATUS_MADS_CHECK_COMPATIBILITY, MADS_BASE_DESC)); - - params.remove("EnforceTorqueControl"); - torque_allowed = false; - } - - madsToggle->setEnabled(_offroad); - madsSettingsButton->setEnabled(madsToggle->isToggled()); - - torqueLateralControlToggle->setEnabled(_offroad && torque_allowed && !nnlcToggle->isToggled()); - torqueLateralControlSettingsButton->setEnabled(torqueLateralControlToggle->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 deleted file mode 100644 index 88c4d401d0..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ /dev/null @@ -1,61 +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/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" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_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 LateralPanel : public QFrame { - Q_OBJECT - -public: - explicit LateralPanel(SettingsWindowSP *parent = nullptr); - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent* event) override; - -public slots: - void updateToggles(bool _offroad); - -private: - Params params; - QStackedLayout* main_layout = nullptr; - QWidget* sunnypilotScreen = nullptr; - ScrollViewSP *sunnypilotScroller = nullptr; - bool offroad; - - ParamControl *madsToggle; - PushButtonSP *madsSettingsButton; - MadsSettings *madsWidget = nullptr; - PushButtonSP *laneChangeSettingsButton; - LaneChangeSettings *laneChangeWidget = nullptr; - NeuralNetworkLateralControl *nnlcToggle = nullptr; - BlinkerPauseLateralSettings *blinkerPauseLateralSettings = nullptr; - ParamControl *torqueLateralControlToggle; - PushButtonSP *torqueLateralControlSettingsButton; - TorqueLateralControlSettings *torqueLateralControlWidget = 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/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc deleted file mode 100644 index 29cadeaa62..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc +++ /dev/null @@ -1,47 +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/longitudinal/custom_acc_increment.h" - -CustomAccIncrement::CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : ExpandableToggleRow(param, title, desc, icon, parent) { - auto *accFrame = new QFrame(this); - auto *accFrameLayout = new QGridLayout(); - accFrame->setLayout(accFrameLayout); - accFrameLayout->setSpacing(0); - - auto *shortPressControl = new AccIncrementOptionControl("CustomAccShortPressIncrement", {1, 10}, 1); - connect(shortPressControl, &OptionControlSP::updateLabels, shortPressControl, &AccIncrementOptionControl::refresh); - - auto *longPressControl = new AccIncrementOptionControl("CustomAccLongPressIncrement", {1, 3}, 1, &customLongValues); - connect(longPressControl, &OptionControlSP::updateLabels, longPressControl, &AccIncrementOptionControl::refresh); - - shortPressControl->setFixedWidth(280); - longPressControl->setFixedWidth(280); - accFrameLayout->addWidget(shortPressControl, 0, 0, Qt::AlignLeft); - accFrameLayout->addWidget(longPressControl, 0, 1, Qt::AlignRight); - - addItem(accFrame); -} - -AccIncrementOptionControl::AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, const int per_value_change, const QMap *valMap) - : OptionControlSP(param, "", "", "", range, per_value_change, true, valMap) { - param_name = param.toStdString(); - refresh(); -} - -void AccIncrementOptionControl::refresh() { - std::string val = params.get(param_name); - std::string label = ""; - label += param_name == "CustomAccShortPressIncrement" ? "Short Press" : "Long Press"; - label += "
" + val; - label += param_name == "CustomAccShortPressIncrement" - ? (val == "1" ? " (Default)" : "") - : (val == "5" ? " (Default)" : ""); - label += "
"; - setLabel(QString::fromStdString(label)); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h deleted file mode 100644 index 707d4143bc..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h +++ /dev/null @@ -1,41 +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/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 CustomAccIncrement : public ExpandableToggleRow { - Q_OBJECT - -public: - CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); - -private: - QMap customLongValues = { - {"1", "1"}, - {"2", "5"}, // Default - {"3", "10"} - }; -}; - -class AccIncrementOptionControl : public OptionControlSP { - Q_OBJECT - -public: - AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, int per_value_change, const QMap *valMap = nullptr); - void refresh(); - -protected: - std::string param_name; - -private: - Params params; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/helpers.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/helpers.h deleted file mode 100644 index 6c02d627fa..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/helpers.h +++ /dev/null @@ -1,49 +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 - -enum class SpeedLimitOffsetType { - NONE, - FIXED, - PERCENT, -}; - -inline const QString SpeedLimitOffsetTypeTexts[]{ - QT_TRANSLATE_NOOP("SpeedLimitSettings", "None"), - QT_TRANSLATE_NOOP("SpeedLimitSettings", "Fixed"), - QT_TRANSLATE_NOOP("SpeedLimitSettings", "Percent"), -}; - -enum class SpeedLimitSourcePolicy { - CAR_ONLY, - MAP_ONLY, - CAR_FIRST, - MAP_FIRST, - COMBINED, -}; - -inline const QString SpeedLimitSourcePolicyTexts[]{ - QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nOnly"), - QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nOnly"), - QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nFirst"), - QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nFirst"), - QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Combined\nData") -}; - -enum class SpeedLimitMode { - OFF, - INFORMATION, - WARNING, - ASSIST, -}; - -inline const QString SpeedLimitModeTexts[]{ - QT_TRANSLATE_NOOP("SpeedLimitSettings", "Off"), - QT_TRANSLATE_NOOP("SpeedLimitSettings", "Information"), - QT_TRANSLATE_NOOP("SpeedLimitSettings", "Warning"), - QT_TRANSLATE_NOOP("SpeedLimitSettings", "Assist"), -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.cc deleted file mode 100644 index c54ff11f69..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.cc +++ /dev/null @@ -1,56 +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 - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.h" - -SpeedLimitPolicy::SpeedLimitPolicy(QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); - - // Back button - PanelBackButton *back = new PanelBackButton(tr("Back")); - connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); - main_layout->addWidget(back, 0, Qt::AlignLeft); - - main_layout->addSpacing(10); - - ListWidgetSP *list = new ListWidgetSP(this); - - std::vector speed_limit_policy_texts{ - tr(SpeedLimitSourcePolicyTexts[static_cast(SpeedLimitSourcePolicy::CAR_ONLY)].toStdString().c_str()), - tr(SpeedLimitSourcePolicyTexts[static_cast(SpeedLimitSourcePolicy::MAP_ONLY)].toStdString().c_str()), - tr(SpeedLimitSourcePolicyTexts[static_cast(SpeedLimitSourcePolicy::CAR_FIRST)].toStdString().c_str()), - tr(SpeedLimitSourcePolicyTexts[static_cast(SpeedLimitSourcePolicy::MAP_FIRST)].toStdString().c_str()), - tr(SpeedLimitSourcePolicyTexts[static_cast(SpeedLimitSourcePolicy::COMBINED)].toStdString().c_str()) - }; - speed_limit_policy = new ButtonParamControlSP( - "SpeedLimitPolicy", - tr("Speed Limit Source"), - "", - "", - speed_limit_policy_texts, - 250); - list->addItem(speed_limit_policy); - connect(speed_limit_policy, &ButtonParamControlSP::buttonClicked, this, &SpeedLimitPolicy::refresh); - - speedLimitPolicyScroller = new ScrollViewSP(list, this); - main_layout->addWidget(speedLimitPolicyScroller); - refresh(); -}; - -void SpeedLimitPolicy::refresh() { - SpeedLimitSourcePolicy policy_param = static_cast(std::atoi(params.get("SpeedLimitPolicy").c_str())); - speed_limit_policy->setDescription(sourceDescription(policy_param)); -} - -void SpeedLimitPolicy::showEvent(QShowEvent *event) { - speedLimitPolicyScroller->verticalScrollBar()->setValue(0); - refresh(); - speed_limit_policy->showDescription(); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.h deleted file mode 100644 index 219aa53c6e..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.h +++ /dev/null @@ -1,57 +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/sunnypilot/ui.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/helpers.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -class SpeedLimitPolicy : public QWidget { - Q_OBJECT - -public: - explicit SpeedLimitPolicy(QWidget *parent = nullptr); - void refresh(); - void showEvent(QShowEvent *event) override; - -signals: - void backPress(); - -private: - Params params; - ButtonParamControlSP *speed_limit_policy; - ScrollViewSP *speedLimitPolicyScroller; - - static QString sourceDescription(SpeedLimitSourcePolicy type = SpeedLimitSourcePolicy::CAR_ONLY) { - QString car_only = tr("⦿ Car Only: Use Speed Limit data only from Car"); - QString map_only = tr("⦿ Map Only: Use Speed Limit data only from OpenStreetMaps"); - QString car_first = tr("⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps"); - QString map_first = tr("⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car"); - QString combined = tr("⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps"); - - if (type == SpeedLimitSourcePolicy::CAR_ONLY) { - car_only = "" + car_only + ""; - } else if (type == SpeedLimitSourcePolicy::MAP_ONLY) { - map_only = "" + map_only + ""; - } else if (type == SpeedLimitSourcePolicy::CAR_FIRST) { - car_first = "" + car_first + ""; - } else if (type == SpeedLimitSourcePolicy::MAP_FIRST) { - map_first = "" + map_first + ""; - } else { - combined = "" + combined + ""; - } - - return QString("%1
%2
%3
%4
%5") - .arg(car_only) - .arg(map_only) - .arg(car_first) - .arg(map_first) - .arg(combined); - } -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.cc deleted file mode 100644 index 99c227d233..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.cc +++ /dev/null @@ -1,180 +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/longitudinal/speed_limit/speed_limit_settings.h" - -#include "selfdrive/ui/sunnypilot/qt/util.h" - -SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent) { - subPanelFrame = new QFrame(); - QVBoxLayout *subPanelLayout = new QVBoxLayout(subPanelFrame); - subPanelLayout->setContentsMargins(0, 0, 0, 0); - subPanelLayout->setSpacing(0); - - // Back button - PanelBackButton *back = new PanelBackButton(tr("Back")); - connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); - subPanelLayout->addWidget(back, 0, Qt::AlignLeft); - - subPanelLayout->addSpacing(20); - - ListWidgetSP *list = new ListWidgetSP(this, false); - - speedLimitPolicyScreen = new SpeedLimitPolicy(this); - - std::vector speed_limit_mode_texts{ - tr(SpeedLimitModeTexts[static_cast(SpeedLimitMode::OFF)].toStdString().c_str()), - tr(SpeedLimitModeTexts[static_cast(SpeedLimitMode::INFORMATION)].toStdString().c_str()), - tr(SpeedLimitModeTexts[static_cast(SpeedLimitMode::WARNING)].toStdString().c_str()), - tr(SpeedLimitModeTexts[static_cast(SpeedLimitMode::ASSIST)].toStdString().c_str()) - }; - speed_limit_mode_settings = new ButtonParamControlSP( - "SpeedLimitMode", - tr("Speed Limit"), - "", - "", - speed_limit_mode_texts, - 380); - list->addItem(speed_limit_mode_settings); - - list->addItem(horizontal_line()); - list->addItem(vertical_space()); - - speedLimitSource = new PushButtonSP(tr("Customize Source")); - connect(speedLimitSource, &QPushButton::clicked, [&]() { - speedLimitScroller->setLastScrollPosition(); - setCurrentWidget(speedLimitPolicyScreen); - speedLimitPolicyScreen->refresh(); - }); - connect(speedLimitPolicyScreen, &SpeedLimitPolicy::backPress, [&]() { - speedLimitScroller->restoreScrollPosition(); - setCurrentWidget(subPanelFrame); - showEvent(new QShowEvent()); - }); - - speedLimitSource->setFixedWidth(720); - list->addItem(speedLimitSource); - - list->addItem(vertical_space(0)); - list->addItem(horizontal_line()); - - QFrame *offsetFrame = new QFrame(this); - QVBoxLayout *offsetLayout = new QVBoxLayout(offsetFrame); - - std::vector speed_limit_offset_texts{ - tr(SpeedLimitOffsetTypeTexts[static_cast(SpeedLimitOffsetType::NONE)].toStdString().c_str()), - tr(SpeedLimitOffsetTypeTexts[static_cast(SpeedLimitOffsetType::FIXED)].toStdString().c_str()), - tr(SpeedLimitOffsetTypeTexts[static_cast(SpeedLimitOffsetType::PERCENT)].toStdString().c_str()) - }; - speed_limit_offset_settings = new ButtonParamControlSP( - "SpeedLimitOffsetType", - tr("Speed Limit Offset"), - "", - "", - speed_limit_offset_texts, - 500); - - offsetLayout->addWidget(speed_limit_offset_settings); - - speed_limit_offset = new OptionControlSP( - "SpeedLimitValueOffset", - "", - "", - "", - {-30, 30} - ); - offsetLayout->addWidget(speed_limit_offset); - - list->addItem(offsetFrame); - - connect(speed_limit_mode_settings, &ButtonParamControlSP::buttonClicked, this, &SpeedLimitSettings::refresh); - connect(speed_limit_offset, &OptionControlSP::updateLabels, this, &SpeedLimitSettings::refresh); - connect(speed_limit_offset_settings, &ButtonParamControlSP::showDescriptionEvent, speed_limit_offset, &OptionControlSP::showDescription); - connect(speed_limit_offset_settings, &ButtonParamControlSP::buttonClicked, this, &SpeedLimitSettings::refresh); - - refresh(); - speedLimitScroller = new ScrollViewSP(list, this); - subPanelLayout->addWidget(speedLimitScroller); - addWidget(subPanelFrame); - addWidget(speedLimitPolicyScreen); - setCurrentWidget(subPanelFrame); -} - -void SpeedLimitSettings::refresh() { - bool is_release = params.getBool("IsReleaseSpBranch"); - bool is_metric_param = params.getBool("IsMetric"); - SpeedLimitMode speed_limit_mode_param = static_cast(std::atoi(params.get("SpeedLimitMode").c_str())); - SpeedLimitOffsetType offset_type_param = static_cast(std::atoi(params.get("SpeedLimitOffsetType").c_str())); - QString offsetLabel = QString::fromStdString(params.get("SpeedLimitValueOffset")); - - bool sla_available; - 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(); - - bool has_longitudinal_control = hasLongitudinalControl(CP); - bool has_icbm = hasIntelligentCruiseButtonManagement(CP_SP); - - /* - * Speed Limit Assist is available when: - * - has_longitudinal_control or has_icbm, and - * - is not a release branch or not a disallowed brand, and - * - is not always disallowed - */ - bool sla_disallow_in_release = CP.getBrand() == "tesla" && is_release; - bool sla_always_disallow = CP.getBrand() == "rivian"; - sla_available = (has_longitudinal_control || has_icbm) && !sla_disallow_in_release && !sla_always_disallow; - - if (!sla_available && speed_limit_mode_param == SpeedLimitMode::ASSIST) { - params.put("SpeedLimitMode", std::to_string(static_cast(SpeedLimitMode::WARNING))); - } - } else { - sla_available = false; - } - - speed_limit_mode_settings->setDescription(modeDescription(speed_limit_mode_param)); - speed_limit_offset->setDescription(offsetDescription(offset_type_param)); - - if (offset_type_param == SpeedLimitOffsetType::PERCENT) { - offsetLabel += "%"; - } else if (offset_type_param == SpeedLimitOffsetType::FIXED) { - offsetLabel += QString(" %1").arg(is_metric_param ? "km/h" : "mph"); - } - - if (offset_type_param == SpeedLimitOffsetType::NONE) { - speed_limit_offset->setVisible(false); - } else { - speed_limit_offset->setVisible(true); - speed_limit_offset->setLabel(offsetLabel); - speed_limit_offset->showDescription(); - } - - if (sla_available) { - speed_limit_mode_settings->setEnableSelectedButtons(true, convertSpeedLimitModeValues(getSpeedLimitModeValues())); - } else { - speed_limit_mode_settings->setEnableSelectedButtons(true, convertSpeedLimitModeValues( - {SpeedLimitMode::OFF, SpeedLimitMode::INFORMATION, SpeedLimitMode::WARNING})); - } - - speed_limit_mode_settings->refresh(); - speed_limit_mode_settings->showDescription(); - speed_limit_offset->showDescription(); -} - -void SpeedLimitSettings::showEvent(QShowEvent *event) { - refresh(); -} - -void SpeedLimitSettings::hideEvent(QHideEvent *event) { - setCurrentWidget(subPanelFrame); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.h deleted file mode 100644 index 69e85814c2..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.h +++ /dev/null @@ -1,98 +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/sunnypilot/ui.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/helpers.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_policy.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -class SpeedLimitSettings : public QStackedWidget { - Q_OBJECT - -public: - SpeedLimitSettings(QWidget *parent = nullptr); - void refresh(); - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - -signals: - void backPress(); - -private: - Params params; - ScrollViewSP *speedLimitScroller; - QFrame *subPanelFrame; - ButtonParamControlSP *speed_limit_mode_settings; - PushButtonSP *speedLimitSource; - SpeedLimitPolicy *speedLimitPolicyScreen; - ButtonParamControlSP *speed_limit_offset_settings; - OptionControlSP *speed_limit_offset; - bool icbm_available = false; - - static QString offsetDescription(SpeedLimitOffsetType type = SpeedLimitOffsetType::NONE) { - QString none_str = tr("⦿ None: No Offset"); - QString fixed_str = tr("⦿ Fixed: Adds a fixed offset [Speed Limit + Offset]"); - QString percent_str = tr("⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"); - - if (type == SpeedLimitOffsetType::FIXED) { - fixed_str = "" + fixed_str + ""; - } else if (type == SpeedLimitOffsetType::PERCENT) { - percent_str = "" + percent_str + ""; - } else { - none_str = "" + none_str + ""; - } - - return QString("%1
%2
%3") - .arg(none_str) - .arg(fixed_str) - .arg(percent_str); - } - - static QString modeDescription(SpeedLimitMode mode = SpeedLimitMode::OFF) { - QString off_str = tr("⦿ Off: Disables the Speed Limit functions."); - QString info_str = tr("⦿ Information: Displays the current road's speed limit."); - QString warning_str = tr("⦿ Warning: Provides a warning when exceeding the current road's speed limit."); - QString assist_str = tr("⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."); - - if (mode == SpeedLimitMode::ASSIST) { - assist_str = "" + assist_str + ""; - } else if (mode == SpeedLimitMode::WARNING) { - warning_str = "" + warning_str + ""; - } else if (mode == SpeedLimitMode::INFORMATION) { - info_str = "" + info_str + ""; - } else { - off_str = "" + off_str + ""; - } - - return QString("%1
%2
%3
%4") - .arg(off_str) - .arg(info_str) - .arg(warning_str) - .arg(assist_str); - } - - static std::vector getSpeedLimitModeValues() { - std::vector values; - for (int i = static_cast(SpeedLimitMode::OFF); - i <= static_cast(SpeedLimitMode::ASSIST); ++i) { - values.push_back(static_cast(i)); - } - return values; - } - - static std::vector convertSpeedLimitModeValues(const std::vector &modes) { - std::vector values; - for (const auto& mode : modes) { - values.push_back(static_cast(mode)); - } - return values; - } -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc deleted file mode 100644 index ab357a2c16..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc +++ /dev/null @@ -1,198 +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/longitudinal_panel.h" - -#include "selfdrive/ui/sunnypilot/qt/util.h" - -LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) { - 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 = new QStackedLayout(this); - ListWidget *list = new ListWidget(this, false); - - cruisePanelScreen = new QWidget(this); - QVBoxLayout *vlayout = new QVBoxLayout(cruisePanelScreen); - vlayout->setContentsMargins(0, 0, 0, 0); - - cruisePanelScroller = new ScrollViewSP(list, this); - vlayout->addWidget(cruisePanelScroller); - - intelligentCruiseButtonManagement = new ParamControlSP( - "IntelligentCruiseButtonManagement", - tr("Intelligent Cruise Button Management (ICBM) (Alpha)"), - "", - "", - this - ); - QObject::connect(intelligentCruiseButtonManagement, &ParamControlSP::toggleFlipped, this, [=](bool) { - refresh(offroad); - }); - list->addItem(intelligentCruiseButtonManagement); - - dynamicExperimentalControl = new ParamControlSP( - "DynamicExperimentalControl", - tr("Dynamic Experimental Control (DEC)"), - tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), - "", - this - ); - list->addItem(dynamicExperimentalControl); - - SmartCruiseControlVision = new ParamControl( - "SmartCruiseControlVision", - tr("Smart Cruise Control - Vision"), - tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."), - ""); - list->addItem(SmartCruiseControlVision); - - SmartCruiseControlMap = new ParamControl( - "SmartCruiseControlMap", - tr("Smart Cruise Control - Map"), - tr("Use map data to estimate the appropriate speed to drive through turns ahead."), - ""); - list->addItem(SmartCruiseControlMap); - - customAccIncrement = new CustomAccIncrement("CustomAccIncrementsEnabled", tr("Custom ACC Speed Increments"), "", "", this); - list->addItem(customAccIncrement); - - QObject::connect(uiState(), &UIState::offroadTransition, this, &LongitudinalPanel::refresh); - - speedLimitSettings = new PushButtonSP(tr("Speed Limit"), 750, this); - connect(speedLimitSettings, &QPushButton::clicked, [&]() { - cruisePanelScroller->setLastScrollPosition(); - main_layout->setCurrentWidget(speedLimitScreen); - }); - list->addItem(speedLimitSettings); - - speedLimitScreen = new SpeedLimitSettings(this); - connect(speedLimitScreen, &SpeedLimitSettings::backPress, [=]() { - cruisePanelScroller->restoreScrollPosition(); - main_layout->setCurrentWidget(cruisePanelScreen); - }); - - main_layout->addWidget(cruisePanelScreen); - main_layout->addWidget(speedLimitScreen); - main_layout->setCurrentWidget(cruisePanelScreen); - refresh(offroad); -} - -void LongitudinalPanel::showEvent(QShowEvent *event) { - main_layout->setCurrentWidget(cruisePanelScreen); - refresh(offroad); -} - -void LongitudinalPanel::hideEvent(QHideEvent *event) { - main_layout->setCurrentWidget(cruisePanelScreen); -} - -void LongitudinalPanel::refresh(bool _offroad) { - const QString icbm_description = tr("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control."); - - 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(); - - has_longitudinal_control = hasLongitudinalControl(CP); - is_pcm_cruise = CP.getPcmCruise(); - has_icbm = hasIntelligentCruiseButtonManagement(CP_SP); - - if (CP_SP.getIntelligentCruiseButtonManagementAvailable() && !has_longitudinal_control) { - intelligentCruiseButtonManagement->setEnabled(offroad); - intelligentCruiseButtonManagement->setDescription(icbm_description); - } else { - params.remove("IntelligentCruiseButtonManagement"); - intelligentCruiseButtonManagement->setEnabled(false); - - const QString icbm_unavaialble = tr("Intelligent Cruise Button Management is currently unavailable on this platform."); - - QString long_desc = icbm_unavaialble; - if (has_longitudinal_control) { - if (CP.getAlphaLongitudinalAvailable()) { - long_desc = icbm_unavaialble + " " + tr("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management."); - } else { - long_desc = icbm_unavaialble + " " + tr("sunnypilot Longitudinal Control is the default longitudinal control for this platform."); - } - } - - intelligentCruiseButtonManagement->setDescription("" + long_desc + "

" + icbm_description); - intelligentCruiseButtonManagement->showDescription(); - } - - if (has_longitudinal_control || has_icbm) { - // enable Custom ACC Increments when long is available and is not PCM cruise - customAccIncrement->setEnabled(((has_longitudinal_control && !is_pcm_cruise) || has_icbm) && offroad); - dynamicExperimentalControl->setEnabled(has_longitudinal_control); - SmartCruiseControlVision->setEnabled(true); - SmartCruiseControlMap->setEnabled(true); - } else { - params.remove("CustomAccIncrementsEnabled"); - params.remove("DynamicExperimentalControl"); - params.remove("SmartCruiseControlVision"); - params.remove("SmartCruiseControlMap"); - customAccIncrement->setEnabled(false); - dynamicExperimentalControl->setEnabled(false); - SmartCruiseControlVision->setEnabled(false); - SmartCruiseControlMap->setEnabled(false); - } - - intelligentCruiseButtonManagement->refresh(); - customAccIncrement->refresh(); - dynamicExperimentalControl->refresh(); - SmartCruiseControlVision->refresh(); - SmartCruiseControlMap->refresh(); - } else { - has_longitudinal_control = false; - is_pcm_cruise = false; - has_icbm = false; - intelligentCruiseButtonManagement->setDescription("" + tr("Start the vehicle to check vehicle compatibility.") + "
" + icbm_description); - } - - QString accEnabledDescription = tr("Enable custom Short & Long press increments for cruise speed increase/decrease."); - QString accNoLongDescription = tr("This feature can only be used with sunnypilot longitudinal control enabled."); - QString accPcmCruiseDisabledDescription = tr("This feature is not supported on this platform due to vehicle limitations."); - QString onroadOnlyDescription = tr("Start the vehicle to check vehicle compatibility."); - - if (offroad) { - customAccIncrement->setDescription(onroadOnlyDescription); - customAccIncrement->showDescription(); - } else { - if (has_longitudinal_control || has_icbm) { - if (has_longitudinal_control && is_pcm_cruise) { - customAccIncrement->setDescription(accPcmCruiseDisabledDescription); - customAccIncrement->showDescription(); - } else { - customAccIncrement->setDescription(accEnabledDescription); - } - } else { - customAccIncrement->toggleFlipped(false); - customAccIncrement->setDescription(accNoLongDescription); - customAccIncrement->showDescription(); - } - } - - offroad = _offroad; -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h deleted file mode 100644 index 2b9c0b8968..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h +++ /dev/null @@ -1,41 +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/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -class LongitudinalPanel : public QWidget { - Q_OBJECT - -public: - explicit LongitudinalPanel(QWidget *parent = nullptr); - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - void refresh(bool _offroad); - -private: - Params params; - bool has_longitudinal_control = false; - bool is_pcm_cruise = false; - bool has_icbm = false; - bool offroad = false; - - QStackedLayout *main_layout = nullptr; - ScrollViewSP *cruisePanelScroller = nullptr; - QWidget *cruisePanelScreen = nullptr; - CustomAccIncrement *customAccIncrement = nullptr; - ParamControl *SmartCruiseControlVision; - ParamControl *SmartCruiseControlMap; - ParamControl *intelligentCruiseButtonManagement = nullptr; - ParamControl *dynamicExperimentalControl = nullptr; - SpeedLimitSettings *speedLimitScreen; - PushButtonSP *speedLimitSettings; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.cc deleted file mode 100644 index 4cc7351b03..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.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 "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 deleted file mode 100644 index 31c6a335c7..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h +++ /dev/null @@ -1,25 +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/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/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc deleted file mode 100644 index 4d89ea58b0..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ /dev/null @@ -1,517 +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 -#include -#include -#include -#include - -#include "common/model.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -static const QString progressStyleActive = "QProgressBar {" - " font-size: 40px;" - " font-weight: 200;" - " padding: 1px;" - " border: 3px solid black;" - " border-radius: 10px;" - "}" - "QProgressBar::chunk {" - " background-color: #1e79e8;" - " border-radius: 10px;" - "}"; - -static const QString progressStyleInactive = progressStyleActive + - "QProgressBar::chunk {" - " background-color: transparent;" - "}"; - -static const QString progressStyleDone = progressStyleActive + - "QProgressBar {" - " color: #33ab4c;" - "}" - "QProgressBar::chunk {" - " background-color: transparent;" - "}"; - -static const QString progressStyleError = progressStyleActive + - "QProgressBar {" - " color: red;" - "}" - "QProgressBar::chunk {" - " background-color: transparent;" - "}"; - -ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(50, 20, 50, 20); - - ListWidgetSP *list = new ListWidgetSP(this, false); - ScrollViewSP *scroller = new ScrollViewSP(list, this); - main_layout->addWidget(scroller); - - const auto current_model = GetActiveModelName(); - currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), "", this); - 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); - - refreshAvailableModelsBtn = new ButtonControlSP(tr("Refresh Model List"), tr("REFRESH"), "", this); - connect(refreshAvailableModelsBtn, &ButtonControlSP::clicked, this, [=]() { - params.put("ModelManager_LastSyncTime", "0"); - ConfirmationDialog::alert(tr("Fetching Latest Models"), this); - }); - - list->addItem(refreshAvailableModelsBtn); - - clearModelCacheBtn = new ButtonControlSP(tr("Clear Model Cache"), tr("CLEAR"), "", this); - connect(clearModelCacheBtn, &ButtonControlSP::clicked, this, &ModelsPanel::clearModelCache); - - list->addItem(clearModelCacheBtn); - - // Create progress bars for downloads - supercomboProgressBar = createProgressBar(this); - QString supercomboType = tr("Driving Model"); - supercomboFrame = createModelDetailFrame(this, supercomboType, supercomboProgressBar); - list->addItem(supercomboFrame); - - navigationProgressBar = createProgressBar(this); - QString navigationType = tr("Navigation Model"); - navigationFrame = createModelDetailFrame(this, navigationType, navigationProgressBar); - list->addItem(navigationFrame); - - visionProgressBar = createProgressBar(this); - QString visionType = tr("Vision Model"); - visionFrame = createModelDetailFrame(this, visionType, visionProgressBar); - list->addItem(visionFrame); - - policyProgressBar = createProgressBar(this); - QString policyType = tr("Policy Model"); - policyFrame = createModelDetailFrame(this, policyType, policyProgressBar); - list->addItem(policyFrame); - list->addItem(horizontal_line()); - - // Lane Turn Desire toggle - lane_turn_desire_toggle = new ParamControlSP("LaneTurnDesire", tr("Use Lane Turn Desires"), - "If you’re driving at 20 mph (32 km/h) or below and have your blinker on, " - "the car will plan a turn in that direction at the nearest drivable path. " - "This prevents situations (like at red lights) where the car might plan the wrong turn direction.", - "../assets/offroad/icon_shell.png"); - list->addItem(lane_turn_desire_toggle); - - // Lane Turn Value control - int max_value_mph = 20; - bool is_metric_initial = params.getBool("IsMetric"); - const float K = 1.609344f; - int per_value_change_scaled = is_metric_initial ? static_cast(std::round((1.0f / K) * 100.0f)) : 100; // 100 -> 1 mph - lane_turn_value_control = new OptionControlSP("LaneTurnValue", tr("Adjust Lane Turn Speed"), - tr("Set the maximum speed for lane turn desires. Default is 19 %1.").arg(is_metric_initial ? "km/h" : "mph"), - "", {5 * 100, max_value_mph * 100}, per_value_change_scaled, false, nullptr, true, true); - lane_turn_value_control->showDescription(); - list->addItem(lane_turn_value_control); - - // Show based on toggle - refreshLaneTurnValueControl(); - connect(lane_turn_desire_toggle, &ParamControlSP::toggleFlipped, this, &ModelsPanel::refreshLaneTurnValueControl); - connect(lane_turn_value_control, &OptionControlSP::updateLabels, this, &ModelsPanel::refreshLaneTurnValueControl); - - // LiveDelay toggle - lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png"); - lagd_toggle_control->showDescription(); - list->addItem(lagd_toggle_control); - - // Software delay control - delay_control = new OptionControlSP("LagdToggleDelay", tr("Adjust Software Delay"), - tr("Adjust the software delay when Live Learning Steer Delay is toggled off." - "\nThe default software delay value is 0.2"), - "", {5, 50}, 1, false, nullptr, true, true); - - connect(delay_control, &OptionControlSP::updateLabels, [=]() { - float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); - delay_control->setLabel(QString::number(value, 'f', 2) + "s"); - }); - connect(lagd_toggle_control, &ParamControlSP::toggleFlipped, [=](bool state) { - delay_control->setVisible(!state); - }); - delay_control->showDescription(); - list->addItem(delay_control); -} - -QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) { - QProgressBar *progressBar = new QProgressBar(parent); - progressBar->setRange(0, 100); - progressBar->setValue(0); - progressBar->setTextVisible(true); - progressBar->setAlignment(Qt::AlignVCenter); - return progressBar; -} - -QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar) { - QFrame *frame = new QFrame(parent); - QHBoxLayout *layout = new QHBoxLayout(frame); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(50); - layout->addWidget(new QLabel(typeName)); - layout->addWidget(progressBar); - frame->setVisible(false); - return frame; -} - -void ModelsPanel::refreshLaneTurnValueControl() { - if (!lane_turn_value_control) return; - float stored_mph = QString::fromStdString(params.get("LaneTurnValue")).toFloat(); - bool is_metric = params.getBool("IsMetric"); - QString unit = is_metric ? "km/h" : "mph"; - float display_value = stored_mph; - if (is_metric) { - display_value = stored_mph * 1.609344f; - } - lane_turn_value_control->setLabel(QString::number(static_cast(std::round(display_value))) + " " + unit); - lane_turn_value_control->setVisible(params.getBool("LaneTurnDesire")); -} - -/** - * @brief Updates the UI with bundle download progress information - * Reads status from modelManagerSP cereal message and displays status for all models - */ -void ModelsPanel::handleBundleDownloadProgress() { - supercomboFrame->setVisible(false); - visionFrame->setVisible(false); - policyFrame->setVisible(false); - navigationFrame->setVisible(false); - - using DS = cereal::ModelManagerSP::DownloadStatus; - if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) { - 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 modelName = QString::fromStdString(bundle.getDisplayName()); - - QProgressBar *progressBar = nullptr; - QFrame *modelFrame = nullptr; - - switch (model.getType()) { - case cereal::ModelManagerSP::Model::Type::SUPERCOMBO: - progressBar = supercomboProgressBar; - modelFrame = supercomboFrame; - break; - case cereal::ModelManagerSP::Model::Type::NAVIGATION: - progressBar = navigationProgressBar; - modelFrame = navigationFrame; - break; - case cereal::ModelManagerSP::Model::Type::VISION: - progressBar = visionProgressBar; - modelFrame = visionFrame; - break; - case cereal::ModelManagerSP::Model::Type::POLICY: - progressBar = policyProgressBar; - modelFrame = policyFrame; - break; - } - - const auto &progress = model.getArtifact().getDownloadProgress(); - QString line; - - if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { - progressBar->setStyleSheet(progressStyleActive); - progressBar->setValue(progress.getProgress()); - progressBar->setFormat(QString(" %1% - %2").arg(static_cast(progress.getProgress())).arg(modelName)); - device()->resetInteractiveTimeout(); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) { - progressBar->setStyleSheet(progressStyleDone); - progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("downloaded") : tr("ready"))); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) { - progressBar->setStyleSheet(progressStyleDone); - progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("from cache") : tr("ready"))); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) { - progressBar->setStyleSheet(progressStyleError); - progressBar->setFormat(tr(" download failed - %1").arg(modelName)); - } else { - progressBar->setStyleSheet(progressStyleInactive); - progressBar->setFormat(tr(" pending - %1").arg(modelName)); - } - // keep navigation hidden for now to avoid confusion - if (model.getType() != cereal::ModelManagerSP::Model::Type::NAVIGATION) { - modelFrame->setVisible(true); - } - } - 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; -} - -/** - * @brief Gets the short name of the currently selected model bundle - * @return Display short name of the selected bundle or default model name - */ -QString ModelsPanel::GetActiveModelInternalName() { - if (model_manager.hasActiveBundle()) { - return QString::fromStdString(model_manager.getActiveBundle().getInternalName()); - } - return DEFAULT_MODEL; -} - -/** - * @brief Gets the ref of the currently selected model bundle - * @return ref of the selected bundle or default model name - */ -QString ModelsPanel::GetActiveModelRef() { - if (model_manager.hasActiveBundle()) { - return QString::fromStdString(model_manager.getActiveBundle().getRef()); - } - - 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...")); - - QList sortedModels; - QSet modelFolders; - QRegularExpression re("\\(([^)]*)\\)[^(]*$"); - - for (const auto &bundle : model_manager.getAvailableBundles()) { - auto overrides = bundle.getOverrides(); - QString folder; - for (const auto &override : overrides) { - if (override.getKey() == "folder") { - folder = QString::fromStdString(override.getValue().cStr()); - } - } - - modelFolders.insert(folder); - sortedModels.append(TreeNode{ - folder, - QString::fromStdString(bundle.getDisplayName()), - QString::fromStdString(bundle.getRef()), - static_cast(bundle.getIndex()) - }); - } - - std::sort(sortedModels.begin(), sortedModels.end(), - [](const TreeNode &a, const TreeNode &b) { - return a.index > b.index; - }); - - // Create a list of folder-maxIndex pairs for sorting - QList> folderMaxIndices; - for (const auto &folder : modelFolders) { - int maxIndex = -1; - for (const auto &model : sortedModels) { - if (model.folder == folder) { - maxIndex = std::max(maxIndex, model.index); - } - } - folderMaxIndices.append(qMakePair(folder, maxIndex)); - } - - // Sort folders by their highest model index - std::sort(folderMaxIndices.begin(), folderMaxIndices.end(), - [](const QPair &a, const QPair &b) { - return a.second > b.second; - }); - - // Create the final items list using sorted folders - QList items; - for (const auto &folderPair : folderMaxIndices) { - QList folderModels; - QString folder = folderPair.first; - for (const auto &model : sortedModels) { - if (model.folder == folderPair.first) { - if (model.index == folderPair.second) { - QRegularExpressionMatch match = re.match(model.displayName); - if (match.hasMatch()) { - folder.append(" - (Updated: ").append(match.captured(1)).append(")"); - } - } - folderModels.append(model); - } - } - items.append(TreeFolder{folder, folderModels}); - } - - items.insert(0, TreeFolder{"", { - TreeNode{"", DEFAULT_MODEL, DEFAULT_MODEL, -1} - }}); - - currentModelLblBtn->setValue(GetActiveModelInternalName()); - - const QString selectedBundleRef = TreeOptionDialog::getSelection( - tr("Select a Model"), items, GetActiveModelRef(), QString("ModelManager_Favs"), this); - - if (selectedBundleRef.isEmpty() || !canContinueOnMeteredDialog()) { - return; - } - - // Handle "Stock" selection differently - if (selectedBundleRef == DEFAULT_MODEL) { - params.remove("ModelManager_ActiveBundle"); - currentModelLblBtn->setValue(tr("Default")); - showResetParamsDialog(); - } else { - // Find selected bundle and initiate download - for (const auto &bundle: model_manager.getAvailableBundles()) { - if (QString::fromStdString(bundle.getRef()) == selectedBundleRef) { - 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(GetActiveModelInternalName()); - - // Update lagdToggle description with current value - QString desc = tr("Enable this for the car to learn and adapt its steering response time. " - "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."); - bool lagdEnabled = params.getBool("LagdToggle"); - if (lagdEnabled) { - auto liveDelayBytes = params.get("LiveDelay"); - if (!liveDelayBytes.empty()) { - auto LD = loadCerealEvent(params, "LiveDelay"); - float lateralDelay = LD->getLiveDelay().getLateralDelay(); - desc += QString("

%1 %2 s") - .arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3)); - } - } else { - auto carParamsBytes = params.get("CarParamsPersistent"); - if (!carParamsBytes.empty()) { - AlignedBuffer aligned_buf_cp; - capnp::FlatArrayMessageReader cmsg(aligned_buf_cp.align(carParamsBytes.data(), carParamsBytes.size())); - cereal::CarParams::Reader CP = cmsg.getRoot(); - - float steerDelay = CP.getSteerActuatorDelay(); - float softwareDelay = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); - float totalLag = steerDelay + softwareDelay; - desc += QString("

" - "%1 %2 s + %3 %4 s = %5 %6 s") - .arg(tr("Actuator Delay:"), QString::number(steerDelay, 'f', 2), - tr("Software Delay:"), QString::number(softwareDelay, 'f', 2), - tr("Total Delay:"), QString::number(totalLag, 'f', 2)); - } - } - lagd_toggle_control->setDescription(desc); - - delay_control->setVisible(!params.getBool("LagdToggle")); - if (delay_control->isVisible()) { - float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); - delay_control->setLabel(QString::number(value, 'f', 2) + "s"); - } - - // Update lane turn desire label and visibility - refreshLaneTurnValueControl(); - - clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB"); -} - -/** - * @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"); - } -} - -void ModelsPanel::clearModelCache() { - QString confirmMsg = tr("This will delete ALL downloaded models from the cache" - "
except the currently active model." - "

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

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


" - "

" + confirmMsg + "

"); - if (showConfirmationDialog( - content, - tr("Clear Cache"))) { - params.putBool("ModelManager_ClearCache", true); - } -} - -double ModelsPanel::calculateCacheSize() { - QFuture future_ModelCacheSize = QtConcurrent::run([=]() { - - QDir model_dir(QString::fromStdString(Path::model_root())); - QFileInfoList model_files = model_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); - qint64 totalSize = 0; - for (const QFileInfo &model_file : model_files) { - if (model_file.isFile()) { - totalSize += model_file.size(); - } - } - return totalSize; - }); - return static_cast(future_ModelCacheSize) / (1024.0 * 1024.0); -} - -void ModelsPanel::showEvent(QShowEvent *event) { - lagd_toggle_control->showDescription(); - if (delay_control->isVisible()) { - delay_control->showDescription(); - } -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h deleted file mode 100644 index 1a39800dde..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h +++ /dev/null @@ -1,88 +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/qt/util.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" - -class ModelsPanel : public QWidget { - Q_OBJECT - -public: - explicit ModelsPanel(QWidget *parent = nullptr); - -private: - QString GetActiveModelName(); - QString GetActiveModelInternalName(); - QString GetActiveModelRef(); - void updateModelManagerState(); - void showEvent(QShowEvent *event) override; - - 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 refreshLaneTurnValueControl(); - void showResetParamsDialog(); - QProgressBar* createProgressBar(QWidget *parent); - QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar); - cereal::ModelManagerSP::Reader model_manager; - cereal::ModelManagerSP::DownloadStatus download_status{}; - cereal::ModelManagerSP::DownloadStatus prev_download_status{}; - void clearModelCache(); - double calculateCacheSize(); - - bool canContinueOnMeteredDialog() { - if (!is_metered) return true; - 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; - ParamControlSP *lagd_toggle_control; - OptionControlSP *delay_control; - QProgressBar *supercomboProgressBar; - QFrame *supercomboFrame; - QProgressBar *navigationProgressBar; - QFrame *navigationFrame; - QProgressBar *visionProgressBar; - QFrame *visionFrame; - QProgressBar *policyProgressBar; - QFrame *policyFrame; - Params params; - ButtonControlSP *clearModelCacheBtn; - ButtonControlSP *refreshAvailableModelsBtn; - ParamControlSP *lane_turn_desire_toggle; - OptionControlSP *lane_turn_value_control; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h deleted file mode 100644 index 32d169ad15..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h +++ /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. - */ - -#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 deleted file mode 100644 index b67e41e09d..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc +++ /dev/null @@ -1,158 +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/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 deleted file mode 100644 index 73890e9d5a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h +++ /dev/null @@ -1,143 +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 // 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 deleted file mode 100644 index e946d7870f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc +++ /dev/null @@ -1,307 +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/osm_panel.h" - -#include -#include -#include - -#include "common/swaglog.h" -#include "selfdrive/ui/sunnypilot/qt/util.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)); - } - - InputDialog d(tr("Search Country"), this, tr("Enter search keywords, or leave blank to list all countries."), false); - d.setMinLength(0); - const int ret = d.exec(); - if (ret) { - const QString selection = search(d.text(), locationTitles, tr("Select Country")); - 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; - } - 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)); - } - - InputDialog d(tr("Search State"), this, tr("Enter search keywords, or leave blank to list all states."), false); - d.setMinLength(0); - const int ret = d.exec(); - if (ret) { - const QString selection = search(d.text(), locationTitles, tr("Select State")); - 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); - } -} - -QString OsmPanel::search(const QString &query, const QStringList &list, const QString &prompt_text) { - QStringList lst_results = searchFromList(query, list); - QString selection; - - if (lst_results.isEmpty()) { - ConfirmationDialog::alert(tr("No results found for keywords: %1").arg(query), this); - return selection; - } - selection = MultiOptionDialog::getSelection(prompt_text, lst_results, "", this); - return selection; -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h deleted file mode 100644 index e4067d415f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h +++ /dev/null @@ -1,233 +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 -#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); - QString search(const QString &query, const QStringList &list, const QString &prompt_text); - - 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 deleted file mode 100644 index c712442df3..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ /dev/null @@ -1,171 +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/settings.h" - -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" -#include "selfdrive/ui/qt/offroad/firehose.h" -#include "selfdrive/ui/sunnypilot/qt/network/networking.h" - -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/display_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" -#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" - -TogglesPanelSP::TogglesPanelSP(SettingsWindowSP *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, &SettingsWindowSP::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 - DevicePanelSP *device = new DevicePanelSP(this); - QObject::connect(device, &DevicePanelSP::reviewTrainingGuide, this, &SettingsWindowSP::reviewTrainingGuide); - QObject::connect(device, &DevicePanelSP::showDriverView, this, &SettingsWindowSP::showDriverView); - - TogglesPanelSP *toggles = new TogglesPanelSP(this); - QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); - QObject::connect(this, &SettingsWindowSP::scrollToToggle, toggles, &TogglesPanel::scrollToToggle); - - 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"), - PanelInfo(" " + tr("Network"), networking, "../assets/icons/network.png"), - 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("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"), - PanelInfo(" " + tr("Display"), new DisplayPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_display.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"), - PanelInfo(" " + tr("Developer"), new DeveloperPanelSP(this), "../assets/icons/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"))) ? 50 : 0; // Network 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 deleted file mode 100644 index ff25d3b64f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h +++ /dev/null @@ -1,43 +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" - -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 - -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(SettingsWindowSP *parent); - -private slots: - void updateState(const UIStateSP &s); -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc deleted file mode 100644 index 8bdb7703f2..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ /dev/null @@ -1,114 +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/software_panel.h" - -SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { - // branch selector - QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr); - connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() { - if (Hardware::get_device_type() == cereal::InitData::DeviceType::TICI) { - auto current = params.get("GitBranch"); - QStringList allBranches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); - QStringList branches; - for (const QString &b : allBranches) { - if (b.endsWith("-tici")) { - branches.append(b); - } - } - - for (QString b : {current.c_str(), "master-tici", "staging-tici", "release-tici"}) { - auto i = branches.indexOf(b); - if (i >= 0) { - branches.removeAt(i); - branches.insert(0, b); - } - } - - QString cur = QString::fromStdString(params.get("UpdaterTargetBranch")); - QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this); - if (!selection.isEmpty()) { - params.put("UpdaterTargetBranch", selection.toStdString()); - targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); - checkForUpdates(); - } - } else { - 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()); - } - } - }); - - // Disable Updates toggle - disableUpdatesToggle = new ParamControl("DisableUpdates", - tr("Disable Updates"), - tr("When enabled, software updates will be disabled. This requires a reboot to take effect."), - "../assets/icons/icon_warning.png", - this, true); - disableUpdatesToggle->showDescription(); - addItem(disableUpdatesToggle); - connect(disableUpdatesToggle, &ParamControl::toggleFlipped, this, &SoftwarePanelSP::handleDisableUpdatesToggled); - connect(uiState(), &UIState::offroadTransition, this, &SoftwarePanelSP::updateDisableUpdatesToggle); - updateDisableUpdatesToggle(!uiState()->scene.started); -} - -/** - * @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(); - } -} - -void SoftwarePanelSP::handleDisableUpdatesToggled(bool state) { - if (ConfirmationDialog::confirm(tr("%1 updates requires a reboot.
Reboot now?") - .arg(state ? "Disabling" : "Enabling"), tr("Reboot"), this)) { - params.putBool("DoReboot", true); - } else { - params.putBool("DisableUpdates", !state); - disableUpdatesToggle->refresh(); - } -} - -void SoftwarePanelSP::updateDisableUpdatesToggle(bool offroad) { - bool enabled = offroad; - disableUpdatesToggle->setEnabled(enabled); - disableUpdatesToggle->setDescription(enabled - ? tr("When enabled, software updates will be disabled.
This requires a reboot to take effect.") - : tr("Please enable always offroad mode or turn off vehicle to adjust these toggles")); -} - -void SoftwarePanelSP::showEvent(QShowEvent *event) { - SoftwarePanel::showEvent(event); - updateDisableUpdatesToggle(!uiState()->scene.started); - disableUpdatesToggle->showDescription(); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h deleted file mode 100644 index efaa836a21..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ /dev/null @@ -1,28 +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/sunnypilot/ui.h" -#include "selfdrive/ui/sunnypilot/qt/util.h" -#include "selfdrive/ui/qt/offroad/settings.h" - -class SoftwarePanelSP final : public SoftwarePanel { - Q_OBJECT - -public: - explicit SoftwarePanelSP(QWidget *parent = nullptr); - -private: - void searchBranches(const QString &query); - ParamControl *disableUpdatesToggle = nullptr; - void handleDisableUpdatesToggled(bool state); -private slots: - void updateDisableUpdatesToggle(bool offroad); -protected: - void showEvent(QShowEvent *event) override; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc deleted file mode 100644 index e29e89ee98..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) 2025-, sunnypilot contributors. - * - * This 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/community_widget.h" - -#include "selfdrive/ui/sunnypilot/ui.h" -#include "selfdrive/ui/sunnypilot/qt/util.h" - -using qrcodegen::QrCode; - -// --- SunnylinkCommunityQRWidget --- - -SunnylinkCommunityQRWidget::SunnylinkCommunityQRWidget(QWidget* parent) - : QWidget(parent) {} - -void SunnylinkCommunityQRWidget::showEvent(QShowEvent *event) { - updateQrCode(SUNNYLINK_COMMUNITY_URL); - update(); -} - -void SunnylinkCommunityQRWidget::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); - } - } - - int final_sz = ((width() / sz) - 1) * sz; - img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly); -} - -void SunnylinkCommunityQRWidget::paintEvent(QPaintEvent *e) { - QPainter p(this); - p.fillRect(rect(), Qt::white); - - if (!img.isNull()) { - QSize s = (size() - img.size()) / 2; - p.drawPixmap(s.width(), s.height(), img); - } -} - -// --- SunnylinkCommunityPopup --- - -QStringList SunnylinkCommunityPopup::getInstructions() { - QStringList instructions; - instructions << tr("Scan the QR code and join us!"); - return instructions; -} - -SunnylinkCommunityPopup::SunnylinkCommunityPopup(QWidget* parent) - : DialogBase(parent) { - auto *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - // Solarized Light base3 background - setStyleSheet("SunnylinkCommunityPopup { background-color: #FDF6E3; }"); - - // Header spanning full width - auto headerWidget = new QWidget(this); - auto headerLayout = new QHBoxLayout(headerWidget); - headerLayout->setContentsMargins(85, 50, 85, 30); - headerLayout->setSpacing(30); - - auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this); - close->setIconSize(QSize(80, 80)); - close->setStyleSheet("border: none;"); - connect(close, &QPushButton::clicked, this, &QDialog::reject); - headerLayout->addWidget(close, 0, Qt::AlignLeft | Qt::AlignVCenter); - - const auto title = new QLabel(tr("Join the sunnypilot Community Forum"), this); - // Solarized base02 for text - title->setStyleSheet("font-size: 65px; color: #073642;"); - title->setWordWrap(false); - title->setAlignment(Qt::AlignCenter); - headerLayout->addWidget(title, 1); - - // Spacer to balance the close button on the right - auto spacer = new QWidget(this); - spacer->setFixedSize(80, 80); - headerLayout->addWidget(spacer, 0); - - mainLayout->addWidget(headerWidget); - - // Two-column content layout - auto contentLayout = new QHBoxLayout(); - contentLayout->setContentsMargins(0, 0, 0, 0); - contentLayout->setSpacing(0); - mainLayout->addLayout(contentLayout, 66); - - // Left side: description - auto leftLayout = new QVBoxLayout(); - leftLayout->setContentsMargins(85, 40, 50, 70); - leftLayout->setSpacing(35); - contentLayout->addLayout(leftLayout, 40); - - // Hype / intro paragraph - const auto desc = new QLabel(tr( - "We're excited to announce our sunnypilot Community Forum

" - "Over the years, Discord just hasn't scaled well for our growing community.
" - "It's noisy, unsearchable, and great discussions disappear too easily.
" - "Our new community forum aims to fix that by making it easier to find answers, share ideas, track feedback, report bugs, help newcomers and more!

" - "Here's what's waiting for you:
" - "• Fully indexable and discoverable through search engines 🔎
" - "• AI-powered🤖 topic and chat summaries, spam detection, and more
" - "• A trust-level system✅ that rewards meaningful contributions
" - "• Designed to work on your own time.🧘

" - "Scan the QR code on the right and join the discussion!" - ), this); - // Solarized base01 for body text - desc->setStyleSheet("font-size: 40px; color: #586E75;"); - desc->setWordWrap(true); - leftLayout->addWidget(desc); - - leftLayout->addStretch(); - - // Right side: QR code and instructions - auto rightLayout = new QVBoxLayout(); - rightLayout->setContentsMargins(50, 40, 85, 70); - rightLayout->setSpacing(40); - contentLayout->addLayout(rightLayout, 1); - - // QR code (smaller, fixed size) - auto *qr = new SunnylinkCommunityQRWidget(this); - qr->setFixedSize(500, 500); - rightLayout->addStretch(); - rightLayout->addWidget(qr, 0, Qt::AlignCenter); - rightLayout->addStretch(); -} \ No newline at end of file diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h deleted file mode 100644 index 613375d12c..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h +++ /dev/null @@ -1,40 +0,0 @@ -/** -* Copyright (c) 2025-, sunnypilot contributors. - * - * This 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_COMMUNITY_URL = "https://community.sunnypilot.ai/sp-qr"; - -class SunnylinkCommunityQRWidget : public QWidget { - Q_OBJECT - -public: - explicit SunnylinkCommunityQRWidget(QWidget* parent = nullptr); - void paintEvent(QPaintEvent*) override; - -private: - QPixmap img; - void updateQrCode(const QString &text); - void showEvent(QShowEvent *event) override; -}; - -// Popup widget -class SunnylinkCommunityPopup : public DialogBase { - Q_OBJECT - -public: - explicit SunnylinkCommunityPopup(QWidget* parent = nullptr); - -private: - static QStringList getInstructions(); -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc deleted file mode 100644 index a014eddacb..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc +++ /dev/null @@ -1,142 +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/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 our forum at https://community.sunnypilot.ai"); - } 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 Forum at https://community.sunnypilot.ai and reach out to a moderator if you have issues"); - } - 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 deleted file mode 100644 index 9f15a6084f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h +++ /dev/null @@ -1,48 +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/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 deleted file mode 100644 index 1b170bb0c2..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ /dev/null @@ -1,305 +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/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); - 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); - auto vlayout = new QVBoxLayout(sunnylinkScreen); - vlayout->setContentsMargins(50, 20, 50, 20); - - auto *list = new ListWidget(this, false); - - QVBoxLayout *titleLayout = new QVBoxLayout; - QLabel *title = new QLabel(tr("🚀 sunnylink 🚀")); - title->setStyleSheet("font-size: 90px; font-weight: 500; font-family: 'Noto Color Emoji';"); - titleLayout->addWidget(title, 0, Qt::AlignCenter); - - QLabel *sunnylinkDesc = new QLabel("
"+ - tr("For secure backup, restore, and remote configuration")+ "
"); - - QLabel *sponsorMsg = new QLabel("
"+ - tr("Sponsorship isn't required for basic backup/restore") + "
" + - tr("Click the sponsor button for more details")+ "
"); - - sunnylinkDesc->setStyleSheet("font-size: 40px; font-weight: 100; font-family: 'Noto';"); - sponsorMsg->setStyleSheet("font-size: 35px; font-weight: 100; font-family: 'Noto';"); - - titleLayout->addWidget(sunnylinkDesc, 0, Qt::AlignCenter); - titleLayout->addWidget(sponsorMsg, 0, Qt::AlignCenter); - - list->addItem(titleLayout); - - 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); - - 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()); - - QString sunnylinkUploaderDesc = tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)"); - sunnylinkUploaderEnabledBtn = new ParamControlSP( - "EnableSunnylinkUploader", - tr("Enable sunnylink uploader (infrastructure test)"), - sunnylinkUploaderDesc, - "", nullptr, true); - list->addItem(sunnylinkUploaderEnabledBtn); - - 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) { - 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); - - updatePanel(); - }); - - // Backup Settings - backupSettings = new PushButtonSP(tr("Backup Settings"), 750, 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"), 750, 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, 0, Qt::AlignLeft); - settings_layout->addSpacing(10); - settings_layout->addWidget(restoreSettings, 0, Qt::AlignRight); - 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); - - main_layout->addWidget(sunnylinkScreen); - - if (is_sunnylink_enabled) { - startSunnylink(); - } -} - -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") { - 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(); - if (is_sunnylink_enabled) { - startSunnylink(); - } -} - -void SunnylinkPanel::updatePanel() { - if (!isVisible()) { - return; - } - - updateBackupManagerState(); - handleBackupProgress(); - const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A")); - sunnylinkEnabledBtn->setEnabled(!is_onroad); - - 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(); - - 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")); - - bool can_do_uploads = max_current_sponsor_rule.roleTier >= SponsorTier::Novice && is_sunnylink_enabled; - sunnylinkUploaderEnabledBtn->setVisible(can_do_uploads); - sunnylinkUploaderEnabledBtn->setEnabled(can_do_uploads); - - - if (!is_sunnylink_enabled) { - sunnylinkEnabledBtn->setValue(""); - sponsorBtn->setValue(""); - pairSponsorBtn->setValue(""); - } - - update(); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h deleted file mode 100644 index c56d8ebd1a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h +++ /dev/null @@ -1,56 +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/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 { - Q_OBJECT - -public: - explicit SunnylinkPanel(QWidget *parent = nullptr); - void showEvent(QShowEvent *event) override; - void paramsRefresh(const QString ¶m_name, const QString ¶m_value); - void updateBackupManagerState(); - void handleBackupProgress(); - -public slots: - void updatePanel(); - -private: - Params params; - QStackedLayout *main_layout = nullptr; - QWidget *sunnylinkScreen = nullptr; - ScrollViewSP *sunnylinkScroller = nullptr; - SunnylinkSponsorPopup *status_popup; - SunnylinkSponsorPopup *pair_popup; - ButtonControlSP *sponsorBtn; - ButtonControlSP *pairSponsorBtn; - SunnylinkClient *sunnylink_client; - cereal::BackupManagerSP::Reader backup_manager; - - ParamControl *sunnylinkEnabledBtn; - bool is_onroad = 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; - ParamControlSP * sunnylinkUploaderEnabledBtn; - - void stopSunnylink() const; - void startSunnylink() const; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.cc deleted file mode 100644 index dcb16d168d..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.cc +++ /dev/null @@ -1,36 +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/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 deleted file mode 100644 index 67a49e21a4..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h +++ /dev/null @@ -1,25 +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/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/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc deleted file mode 100644 index bebc6c3032..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc +++ /dev/null @@ -1,62 +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/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 deleted file mode 100644 index b4dc9fc81b..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.h +++ /dev/null @@ -1,18 +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/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 deleted file mode 100644 index 03921c01a3..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc +++ /dev/null @@ -1,21 +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/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 deleted file mode 100644 index 4aff123177..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.h +++ /dev/null @@ -1,28 +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/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); - virtual ~BrandSettingsInterface() = default; - - 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/brands.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h deleted file mode 100644 index 795dba5739..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/brands.h +++ /dev/null @@ -1,21 +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/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 deleted file mode 100644 index fd8fc423fe..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.cc +++ /dev/null @@ -1,14 +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/chrysler_settings.h" - -ChryslerSettings::ChryslerSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void ChryslerSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.h deleted file mode 100644 index ab84bec16a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/chrysler_settings.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/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 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 deleted file mode 100644 index 96564cb8a9..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.cc +++ /dev/null @@ -1,14 +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/ford_settings.h" - -FordSettings::FordSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void FordSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.h deleted file mode 100644 index 468bd6b7dd..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/ford_settings.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/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 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 deleted file mode 100644 index 060a28424b..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.cc +++ /dev/null @@ -1,14 +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/gm_settings.h" - -GMSettings::GMSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void GMSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.h deleted file mode 100644 index 211563ab61..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/gm_settings.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/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 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 deleted file mode 100644 index ca18fa7240..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.cc +++ /dev/null @@ -1,14 +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/honda_settings.h" - -HondaSettings::HondaSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void HondaSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.h deleted file mode 100644 index e41602dc11..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/honda_settings.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/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 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 deleted file mode 100644 index 01ea64f0cb..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.cc +++ /dev/null @@ -1,57 +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" - -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::buttonClicked, 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 deleted file mode 100644 index b1ae95a01e..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_settings.h +++ /dev/null @@ -1,65 +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/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" - -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 sunnypilot 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 sunnypilot longitudinal control.")) - .arg(off_str) - .arg(dynamic_str) - .arg(predictive_str); - } -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc deleted file mode 100644 index b8b8825f27..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.cc +++ /dev/null @@ -1,14 +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/mazda_settings.h" - -MazdaSettings::MazdaSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void MazdaSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.h deleted file mode 100644 index cbbc38de7f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/mazda_settings.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/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 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 deleted file mode 100644 index 3c21943622..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.cc +++ /dev/null @@ -1,14 +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/nissan_settings.h" - -NissanSettings::NissanSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void NissanSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.h deleted file mode 100644 index d6eabac0bf..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/nissan_settings.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/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 updateSettings() override; - -private: - bool offroad = false; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc deleted file mode 100644 index 0d4dde681f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc +++ /dev/null @@ -1,235 +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/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); - } - }); - - 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()) { - 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(); - - platform = QString::fromStdString(CP.getCarFingerprint().cStr()); - - for (auto it = platforms.constBegin(); it != platforms.constEnd(); ++it) { - if (it.value()["platform"].toString() == platform) { - 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]; - - 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 deleted file mode 100644 index 9867371ef6..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/platform_selector.h +++ /dev/null @@ -1,68 +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/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 - -public: - PlatformSelector(); - QVariant getPlatformBundle(const QString &key); - - QString platform; - QString brand; - -public slots: - void refresh(bool _offroad); - -signals: - void refreshPanel(); - -private: - void searchPlatforms(const QString &query); - void setPlatform(const QString &platform = ""); - QMap platforms; - - 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); - } -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc deleted file mode 100644 index 92cf3dcd6c..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.cc +++ /dev/null @@ -1,14 +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/rivian_settings.h" - -RivianSettings::RivianSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void RivianSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.h deleted file mode 100644 index be25d01b37..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/rivian_settings.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/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 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 deleted file mode 100644 index 302740a94a..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.cc +++ /dev/null @@ -1,59 +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/subaru_settings.h" - -SubaruSettings::SubaruSettings(QWidget *parent) : BrandSettingsInterface(parent) { - stopAndGoToggle = new ParamControl("SubaruStopAndGo", tr("Stop and Go (Beta)"), "", ""); - stopAndGoToggle->setConfirmation(true, false); - list->addItem(stopAndGoToggle); - - stopAndGoManualParkingBrakeToggle = new ParamControl( - "SubaruStopAndGoManualParkingBrake", - tr("Stop and Go for Manual Parking Brake (Beta)"), - "", - "" - ); - stopAndGoManualParkingBrakeToggle->setConfirmation(true, false); - list->addItem(stopAndGoManualParkingBrakeToggle); -} - -void SubaruSettings::updateSettings() { - 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(); - - is_subaru = CP.getBrand() == "subaru"; - - if (is_subaru) { - if (!(CP.getFlags() & (SUBARU_FLAG_GLOBAL_GEN2 | SUBARU_FLAG_HYBRID))) { - has_stop_and_go = true; - } - } - } else { - is_subaru = false; - has_stop_and_go = false; - } - - bool stop_and_go_disabled = !offroad || !has_stop_and_go; - QString stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc); - QString stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc); - if (stop_and_go_disabled) { - stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc, stopAndGoDisabledMsg()); - stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc, stopAndGoDisabledMsg()); - } - - stopAndGoToggle->setEnabled(has_stop_and_go); - stopAndGoToggle->setDescription(stop_and_go_desc); - stopAndGoToggle->showDescription(); - - stopAndGoManualParkingBrakeToggle->setEnabled(has_stop_and_go); - stopAndGoManualParkingBrakeToggle->setDescription(stop_and_go_manual_parking_brake_desc); - stopAndGoManualParkingBrakeToggle->showDescription(); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h deleted file mode 100644 index 2bb160beba..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h +++ /dev/null @@ -1,57 +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/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" - -const int SUBARU_FLAG_GLOBAL_GEN2 = 4; -const int SUBARU_FLAG_HYBRID = 32; - -class SubaruSettings : public BrandSettingsInterface { - Q_OBJECT - -public: - explicit SubaruSettings(QWidget *parent = nullptr); - void updateSettings() override; - -private: - bool offroad = false; - bool is_subaru; - bool has_stop_and_go; - - ParamControl* stopAndGoToggle; - ParamControl* stopAndGoManualParkingBrakeToggle; - - QString stopAndGoDesc = tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms."); - QString stopAndGoManualParkingBrakeDesc = tr("Experimental feature to enable stop and go for Subaru Global models with manual handbrake. Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!"); - - QString stopAndGoDisabledMsg() const { - if (is_subaru && !has_stop_and_go) { - return tr("This feature is currently not available on this platform."); - } - - if (!is_subaru) { - return tr("Start the car to check car compatibility."); - } - - if (!offroad) { - return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle."); - } - - return QString(); - } - - static QString stopAndGoDescriptionBuilder(const QString &base_description, const QString &custom_description = "") { - return "" + custom_description + "

" + base_description; - } -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc deleted file mode 100644 index 2fffdb8ad4..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.cc +++ /dev/null @@ -1,48 +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/tesla_settings.h" - -TeslaSettings::TeslaSettings(QWidget *parent) : BrandSettingsInterface(parent) { - constexpr int coopSteeringMinKmh = 23; // minimum speed for cooperative steering (enforced by Tesla firmware) - constexpr int oemSteeringMinKmh = 48; // minimum speed for OEM lane departure avoidance (enforced by Tesla firmware) - bool is_metric = params.getBool("IsMetric"); - QString unit = is_metric ? "km/h" : "mph"; - int display_value_coop; - int display_value_oem; - if (is_metric) { - display_value_coop = coopSteeringMinKmh; - display_value_oem = oemSteeringMinKmh; - } else { - display_value_coop = static_cast(std::round(coopSteeringMinKmh * KM_TO_MILE)); - display_value_oem = static_cast(std::round(oemSteeringMinKmh * KM_TO_MILE)); - } - const QString coop_desc = QString("%1

" - "%2
" - "%3
") - .arg(tr("Warning: May experience steering oscillations below %5 %6 during turns, recommend disabling this feature if you experience these.")) - .arg(tr("Allows the driver to provide limited steering input while openpilot is engaged.")) - .arg(tr("Only works above %4 %6.")) - .arg(display_value_coop) - .arg(display_value_oem) - .arg(unit); - - coopSteeringToggle = new ParamControlSP( - "TeslaCoopSteering", - tr("Cooperative Steering (Beta)"), - coop_desc, - "", - this - ); - list->addItem(coopSteeringToggle); - coopSteeringToggle->showDescription(); - coopSteeringToggle->setConfirmation(true, false); -} - -void TeslaSettings::updateSettings() { - coopSteeringToggle->setEnabled(offroad); -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h deleted file mode 100644 index a1294513ee..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.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/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 updateSettings() override; - -private: - ParamControlSP *coopSteeringToggle = nullptr; -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc deleted file mode 100644 index c416177916..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.cc +++ /dev/null @@ -1,14 +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/toyota_settings.h" - -ToyotaSettings::ToyotaSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void ToyotaSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.h deleted file mode 100644 index 9fc18a2a65..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/toyota_settings.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/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 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 deleted file mode 100644 index 59f0aab727..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc +++ /dev/null @@ -1,14 +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/volkswagen_settings.h" - -VolkswagenSettings::VolkswagenSettings(QWidget *parent) : BrandSettingsInterface(parent) { -} - -void VolkswagenSettings::updateSettings() { -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.h deleted file mode 100644 index 25d3a07faf..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.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/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 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 deleted file mode 100644 index 5c00e746f8..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.cc +++ /dev/null @@ -1,67 +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_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) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->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); - main_layout->addWidget(scroller); - - currentBrandSettings = nullptr; - - QObject::connect(uiState(), &UIState::offroadTransition, this, &VehiclePanel::updatePanel); -} - -void VehiclePanel::showEvent(QShowEvent *event) { - updatePanel(offroad); -} - -void VehiclePanel::updatePanel(bool _offroad) { - offroad = _offroad; - platformSelector->refresh(_offroad); - updateBrandSettings(); -} - -void VehiclePanel::updateBrandSettings() { - if (!isVisible()) { - return; - } - - if (currentBrandSettings) { - brandSettingsContainerLayout->removeWidget(currentBrandSettings); - delete currentBrandSettings; - currentBrandSettings = nullptr; - } - - if (BrandSettingsFactory::isBrandSupported(platformSelector->brand)) { - currentBrandSettings = BrandSettingsFactory::createBrandSettings(platformSelector->brand, this); - if (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 deleted file mode 100644 index a170aa87c3..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h +++ /dev/null @@ -1,34 +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/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 { - Q_OBJECT - -public: - explicit VehiclePanel(QWidget *parent = nullptr); - void showEvent(QShowEvent *event) override; - -public slots: - void updatePanel(bool _offroad); - -private: - PlatformSelector* platformSelector = nullptr; - BrandSettingsInterface* currentBrandSettings = nullptr; - QWidget* brandSettingsContainer = nullptr; - QVBoxLayout* brandSettingsContainerLayout = nullptr; - bool offroad = false; - -private slots: - void updateBrandSettings(); -}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc deleted file mode 100644 index 37e982a982..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ /dev/null @@ -1,194 +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/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."), - "", - false, - }, - { - "RainbowMode", - tr("Enable Tesla Rainbow Mode"), - RainbowizeWords(tr("A beautiful rainbow effect on the path the model wants to take.")) + "
" + tr("It")+ " " + tr("does not") + " " + tr("affect driving in any way.") + "", - "", - false, - }, - { - "StandstillTimer", - tr("Enable Standstill Timer"), - tr("Show a timer on the HUD when the car is at a standstill."), - "", - false, - }, - { - "RoadNameToggle", - tr("Display Road Name"), - tr("Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name."), - "", - false, - }, - { - "GreenLightAlert", - tr("Green Traffic Light Alert (Beta)"), - QString("%1
" - "

%2


") - .arg(tr("A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you.")) - .arg(tr("Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly.")), - "", - false, - }, - { - "LeadDepartAlert", - tr("Lead Departure Alert (Beta)"), - QString("%1
" - "

%2


") - .arg(tr("A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving.")) - .arg(tr("Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly.")), - "", - false, - }, - { - "TrueVEgoUI", - tr("Speedometer: Always Display True Speed"), - tr("Always display the true vehicle current speed from wheel speed sensors."), - "", - false, - }, - { - "HideVEgoUI", - tr("Speedometer: Hide from Onroad Screen"), - tr("When enabled, the speedometer on the onroad screen is not displayed."), - "", - false, - }, - { - "ShowTurnSignals", - tr("Display Turn Signals"), - tr("When enabled, visual turn indicators are drawn on the HUD."), - "", - false, - }, - }; - - // Add regular toggles first - 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); - } - - // Visuals: Display Metrics below Chevron - std::vector chevron_info_settings_texts{tr("Off"), tr("Distance"), tr("Speed"), tr("Time"), tr("All")}; - chevron_info_settings = new ButtonParamControlSP( - "ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with sunnypilot longitudinal control)."), - "", - chevron_info_settings_texts, - 200); - chevron_info_settings->showDescription(); - list->addItem(chevron_info_settings); - param_watcher->addParam("ChevronInfo"); - - // Visuals: Developer UI Info (Dev UI) - std::vector dev_ui_settings_texts{tr("Off"), tr("Right"), tr("Right &&\nBottom")}; - dev_ui_settings = new ButtonParamControlSP( - "DevUIInfo", tr("Developer UI"), tr("Display real-time parameters and metrics from various sources."), - "", - dev_ui_settings_texts, - 380); - list->addItem(dev_ui_settings); - - sunnypilotScroller = new ScrollViewSP(list, this); - vlayout->addWidget(sunnypilotScroller); - - main_layout->addWidget(sunnypilotScreen); - - QObject::connect(uiState(), &UIState::offroadTransition, this, &VisualsPanel::refreshLongitudinalStatus); - - refreshLongitudinalStatus(); -} - -void VisualsPanel::refreshLongitudinalStatus() { - 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; - } - - if (chevron_info_settings) { - QString chevronEnabledDescription = tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with sunnypilot longitudinal control)."); - QString chevronNoLongDescription = tr("This feature requires sunnypilot longitudinal control to be available."); - - if (has_longitudinal_control) { - chevron_info_settings->setDescription(chevronEnabledDescription); - } else { - // Reset to "Off" when longitudinal not available - params.put("ChevronInfo", "0"); - chevron_info_settings->setDescription(chevronNoLongDescription); - } - - // Enable only when longitudinal is available - chevron_info_settings->setEnabled(has_longitudinal_control); - chevron_info_settings->refresh(); - } -} - -void VisualsPanel::paramsRefresh() { - if (!isVisible()) { - return; - } - - for (auto toggle : toggles) { - toggle.second->refresh(); - } - - if (chevron_info_settings) { - chevron_info_settings->refresh(); - } - if (dev_ui_settings) { - dev_ui_settings->refresh(); - } -} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h deleted file mode 100644 index 76a846dd45..0000000000 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.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/sunnypilot/qt/offroad/settings/settings.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" - -class ScrollViewSP; - -class VisualsPanel : public QWidget { - Q_OBJECT - -public: - explicit VisualsPanel(QWidget *parent = nullptr); - - void paramsRefresh(); - void refreshLongitudinalStatus(); - -protected: - QStackedLayout* main_layout = nullptr; - QWidget* sunnypilotScreen = nullptr; - ScrollViewSP *sunnypilotScroller = nullptr; - Params params; - std::map toggles; - ParamWatcher * param_watcher; - ButtonParamControlSP *chevron_info_settings; - ButtonParamControlSP *dev_ui_settings; - - bool has_longitudinal_control = false; -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/alerts.cc b/selfdrive/ui/sunnypilot/qt/onroad/alerts.cc deleted file mode 100644 index 695f6d2ce8..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/alerts.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 "selfdrive/ui/sunnypilot/qt/onroad/alerts.h" - -#include -#include -#include -#include - -OnroadAlerts::Alert OnroadAlertsSP::getAlert(const SubMaster &sm, uint64_t started_frame) { - OnroadAlerts::Alert alert = OnroadAlerts::getAlert(sm, started_frame); - alert.text1.replace("openpilot", "sunnypilot"); - alert.text2.replace("openpilot", "sunnypilot"); - return alert; -} - -void OnroadAlertsSP::paintEvent(QPaintEvent *event) { - if (alert.size == cereal::SelfdriveState::AlertSize::NONE) { - return; - } else if (alert.size == cereal::SelfdriveState::AlertSize::FULL) { - OnroadAlerts::paintEvent(event); - return; - } - static std::map alert_heights = { - {cereal::SelfdriveState::AlertSize::SMALL, 271}, - {cereal::SelfdriveState::AlertSize::MID, 420} - }; - int h = alert_heights[alert.size]; - - QPainter p(this); - QFont topFont; - QFont bottomFont; - QRect topTextBoundingRect; - QRect bottomTextBoundingRect; - QRect rect; - - int margin = 40; - int radius = 30; - - const int dev_ui_info = uiStateSP()->scene.dev_ui_info; - const int v_adjustment = dev_ui_info > 1 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 40 : 0; - const int h_adjustment = dev_ui_info > 0 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 230 : 0; - - if (alert.size == cereal::SelfdriveState::AlertSize::SMALL) { - topFont = InterFont(74, QFont::DemiBold); - QFontMetrics fmTop(topFont); - topTextBoundingRect = fmTop.boundingRect( - QRect(0 + margin, height() - h + margin - v_adjustment, width() - margin * 2 - h_adjustment, 0), Qt::TextWordWrap, - alert.text1); - h = topTextBoundingRect.height(); - rect = QRect(0 + margin, height() - h - margin * 2 - v_adjustment, width() - margin * 2 - h_adjustment, h + margin); - } else if (alert.size == cereal::SelfdriveState::AlertSize::MID) { - topFont = InterFont(88, QFont::Bold); - bottomFont = InterFont(66); - QFontMetrics fmTop(topFont); - QFontMetrics fmBotton(bottomFont); - topTextBoundingRect = fmTop.boundingRect( - QRect(0 + margin, height() - h + margin - v_adjustment, width() - margin * 2 - h_adjustment, 0), Qt::TextWordWrap, - alert.text1); - bottomTextBoundingRect = fmBotton.boundingRect( - QRect(0 + margin, height() - h + margin - v_adjustment + topTextBoundingRect.height(), - width() - margin * 2 - h_adjustment, 0), Qt::TextWordWrap, alert.text2); - h = topTextBoundingRect.height() + bottomTextBoundingRect.height() + margin * 2; - rect = QRect(0 + margin, height() - h - margin * 2 - v_adjustment, width() - margin * 2 - h_adjustment, h + margin); - } - - - // draw background + gradient - // draw background + gradient - p.setPen(Qt::NoPen); - p.setCompositionMode(QPainter::CompositionMode_SourceOver); - p.setBrush(QBrush(alert_colors[alert.status])); - p.drawRoundedRect(rect, radius, radius); - - QLinearGradient g(0, rect.y(), 0, rect.bottom()); - g.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.05)); - g.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0.35)); - - p.setCompositionMode(QPainter::CompositionMode_DestinationOver); - p.setBrush(QBrush(g)); - p.drawRoundedRect(rect, radius, radius); - p.setCompositionMode(QPainter::CompositionMode_SourceOver); - - // text - p.setPen(QColor(0xff, 0xff, 0xff)); - p.setRenderHint(QPainter::TextAntialiasing); - p.setFont(topFont); - if (alert.size == cereal::SelfdriveState::AlertSize::SMALL) { - p.drawText(rect, Qt::AlignCenter | Qt::TextWordWrap, alert.text1); - } else if (alert.size == cereal::SelfdriveState::AlertSize::MID) { - QRect topText = QRect(rect.x(), rect.top() + margin, rect.width(), topTextBoundingRect.height()); - p.drawText(topText, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap | Qt::AlignCenter, alert.text1); - p.setFont(bottomFont); - p.drawText(QRect(rect.x(), topText.bottom() + margin, rect.width(), bottomTextBoundingRect.height()), - Qt::AlignHCenter | Qt::TextWordWrap | Qt::AlignCenter, alert.text2); - } -} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/alerts.h b/selfdrive/ui/sunnypilot/qt/onroad/alerts.h deleted file mode 100644 index 8b65309f46..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/alerts.h +++ /dev/null @@ -1,22 +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/alerts.h" -#include "selfdrive/ui/sunnypilot/ui.h" - -class OnroadAlertsSP : public OnroadAlerts { - Q_OBJECT - -public: - OnroadAlertsSP(QWidget *parent = 0) : OnroadAlerts(parent) {} - -protected: - void paintEvent(QPaintEvent *) override; - Alert getAlert(const SubMaster &sm, uint64_t started_frame); -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc deleted file mode 100644 index f9b62b0ae7..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.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 "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); -} - -void AnnotatedCameraWidgetSP::showEvent(QShowEvent *event) { - AnnotatedCameraWidget::showEvent(event); - ui_update_params_sp(uiState()); - uiStateSP()->reset_onroad_sleep_timer(OnroadTimerStatusToggle::RESUME); -} - -void AnnotatedCameraWidgetSP::hideEvent(QHideEvent *event) { - AnnotatedCameraWidget::hideEvent(event); - uiStateSP()->reset_onroad_sleep_timer(OnroadTimerStatusToggle::PAUSE); -} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h deleted file mode 100644 index 2e7609dae0..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h +++ /dev/null @@ -1,22 +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/annotated_camera.h" - -class AnnotatedCameraWidgetSP : public AnnotatedCameraWidget { - Q_OBJECT - -public: - explicit AnnotatedCameraWidgetSP(VisionStreamType type, QWidget *parent = nullptr); - void updateState(const UIState &s) override; - -protected: - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent* event) override; -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/buttons.cc b/selfdrive/ui/sunnypilot/qt/onroad/buttons.cc deleted file mode 100644 index 97f6a391ce..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/buttons.cc +++ /dev/null @@ -1,51 +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/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 deleted file mode 100644 index ce62a69e2d..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/buttons.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/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/qt/onroad/developer_ui/developer_ui.cc b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc deleted file mode 100644 index 292ba6f7bb..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc +++ /dev/null @@ -1,227 +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 - -#include "common/util.h" -#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" - - -// Add Relative Distance to Primary Lead Car -// Unit: Meters -UiElement DeveloperUi::getDRel(bool lead_status, float lead_d_rel) { - QString value = lead_status ? QString::number(lead_d_rel, 'f', 0) : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lead_status) { - // Orange if close, Red if very close - if (lead_d_rel < 5) { - color = QColor(255, 0, 0, 255); - } else if (lead_d_rel < 15) { - color = QColor(255, 188, 0, 255); - } - } - - return UiElement(value, "REL DIST", "m", color); -} - -// Add Relative Velocity vs Primary Lead Car -// Unit: kph if metric, else mph -UiElement DeveloperUi::getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit) { - QString value = lead_status ? QString::number(lead_v_rel * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lead_status) { - // Red if approaching faster than 10mph - // Orange if approaching (negative) - if (lead_v_rel < -4.4704) { - color = QColor(255, 0, 0, 255); - } else if (lead_v_rel < 0) { - color = QColor(255, 188, 0, 255); - } - } - - return UiElement(value, "REL SPEED", speed_unit, color); -} - -// Add Real Steering Angle -// Unit: Degrees -UiElement DeveloperUi::getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override) { - QString value = QString("%1%2%3").arg(QString::number(angle_steers, 'f', 1)).arg("°").arg(""); - QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); - - // Red if large steering angle - // Orange if moderate steering angle - if (std::fabs(angle_steers) > 180) { - color = QColor(255, 0, 0, 255); - } else if (std::fabs(angle_steers) > 90) { - color = QColor(255, 188, 0, 255); - } - - return UiElement(value, "REAL STEER", "", color); -} - -// Add Actual Lateral Acceleration (roll compensated) when using Torque -// Unit: m/s² -UiElement DeveloperUi::getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override) { - double actualLateralAccel = (curvature * pow(v_ego, 2)) - (roll * 9.81); - - QString value = QString::number(actualLateralAccel, 'f', 2); - QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); - - return UiElement(value, "ACTUAL L.A.", "m/s²", color); -} - -// Add Desired Steering Angle when using PID -// Unit: Degrees -UiElement DeveloperUi::getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers) { - QString value = lat_active ? QString("%1%2%3").arg(QString::number(steer_angle_desired, 'f', 1)).arg("°").arg("") : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lat_active) { - // Red if large steering angle - // Orange if moderate steering angle - if (std::fabs(angle_steers) > 180) { - color = QColor(255, 0, 0, 255); - } else if (std::fabs(angle_steers) > 90) { - color = QColor(255, 188, 0, 255); - } else { - color = QColor(0, 255, 0, 255); - } - } - - return UiElement(value, "DESIRED STEER", "", color); -} - -// Add Device Memory (RAM) Usage -// Unit: Percent -UiElement DeveloperUi::getMemoryUsagePercent(int memory_usage_percent) { - QString value = QString("%1%2").arg(QString::number(memory_usage_percent, 'd', 0)).arg("%"); - QColor color = (memory_usage_percent > 85) ? QColor(255, 188, 0, 255) : QColor(255, 255, 255, 255); - - return UiElement(value, "RAM", "", color); -} - -// Add Vehicle Current Acceleration -// Unit: m/s² -UiElement DeveloperUi::getAEgo(float a_ego) { - QString value = QString::number(a_ego, 'f', 1); - QColor color = QColor(255, 255, 255, 255); - - return UiElement(value, "ACC.", "m/s²", color); -} - -// Add Relative Velocity to Primary Lead Car -// Unit: kph if metric, else mph -UiElement DeveloperUi::getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit) { - QString value = lead_status ? QString::number((lead_v_rel + v_ego) * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lead_status) { - // Red if approaching faster than 10mph - // Orange if approaching (negative) - if (lead_v_rel < -4.4704) { - color = QColor(255, 0, 0, 255); - } else if (lead_v_rel < 0) { - color = QColor(255, 188, 0, 255); - } - } - - return UiElement(value, "L.S.", speed_unit, color); -} - -// Add Friction Coefficient Raw from torqued -// Unit: None -UiElement DeveloperUi::getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid) { - QString value = QString::number(friction_coefficient_filtered, 'f', 3); - QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); - - return UiElement(value, "FRIC.", "", color); -} - -// Add Lateral Acceleration Factor Raw from torqued -// Unit: m/s² -UiElement DeveloperUi::getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid) { - QString value = QString::number(lat_accel_factor_filtered, 'f', 3); - QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); - - return UiElement(value, "L.A.", "m/s²", color); -} - -// Add Steering Torque from Car EPS -// Unit: Newton Meters -UiElement DeveloperUi::getSteeringTorqueEps(float steering_torque_eps) { - QString value = QString::number(std::fabs(steering_torque_eps), 'f', 1); - QColor color = QColor(255, 255, 255, 255); - - return UiElement(value, "E.T.", "N·dm", color); -} - -// Add Bearing Degree and Direction from Car (Compass) -// Unit: Meters -UiElement DeveloperUi::getBearingDeg(float bearing_accuracy_deg, float bearing_deg) { - QString value = (bearing_accuracy_deg != 180.00) ? QString("%1%2%3").arg(QString::number(bearing_deg, 'd', 0)).arg("°").arg("") : "-"; - QColor color = QColor(255, 255, 255, 255); - QString dir_value; - - if (bearing_accuracy_deg != 180.00) { - if (((bearing_deg >= 337.5) && (bearing_deg <= 360)) || ((bearing_deg >= 0) && (bearing_deg <= 22.5))) { - dir_value = "N"; - } else if ((bearing_deg > 22.5) && (bearing_deg < 67.5)) { - dir_value = "NE"; - } else if ((bearing_deg >= 67.5) && (bearing_deg <= 112.5)) { - dir_value = "E"; - } else if ((bearing_deg > 112.5) && (bearing_deg < 157.5)) { - dir_value = "SE"; - } else if ((bearing_deg >= 157.5) && (bearing_deg <= 202.5)) { - dir_value = "S"; - } else if ((bearing_deg > 202.5) && (bearing_deg < 247.5)) { - dir_value = "SW"; - } else if ((bearing_deg >= 247.5) && (bearing_deg <= 292.5)) { - dir_value = "W"; - } else if ((bearing_deg > 292.5) && (bearing_deg < 337.5)) { - dir_value = "NW"; - } - } else { - dir_value = "OFF"; - } - - return UiElement(QString("%1 | %2").arg(dir_value).arg(value), "B.D.", "", color); -} - -// Add Altitude of Current Location -// Unit: Meters -UiElement DeveloperUi::getAltitude(float gps_accuracy, float altitude) { - QString value = (gps_accuracy != 0.00) ? QString::number(altitude, 'f', 1) : "-"; - QColor color = QColor(255, 255, 255, 255); - - return UiElement(value, "ALT.", "m", color); -} - -// Add Actuators Output -// Unit: Degree (angle) or m/s² (torque) -UiElement DeveloperUi::getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, - cereal::CarControl::Actuators::Reader &actuators, - float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override) { - QString label; - QString value; - QString unit; - - if (steerControlType == cereal::CarParams::SteerControlType::ANGLE) { - label = "DESIRED STEER"; - value = QString("%1%2%3").arg(QString::number(actuators.getSteeringAngleDeg(), 'f', 1)).arg("°").arg(""); - } else { - label = "DESIRED L.A."; - double desiredLateralAccel = (desiredCurvature * pow(v_ego, 2)) - (roll * 9.81); - value = QString::number(desiredLateralAccel, 'f', 2); - unit = "m/s²"; - } - - value = lat_active ? value : "-"; - QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); - - return UiElement(value, label, unit, color); -} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h deleted file mode 100644 index 0c5c472209..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h +++ /dev/null @@ -1,31 +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/qt/onroad/developer_ui/ui_elements.h" - -class DeveloperUi { - -public: - static UiElement getDRel(bool lead_status, float lead_d_rel); - static UiElement getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit); - static UiElement getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override); - static UiElement getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override); - static UiElement getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers); - static UiElement getMemoryUsagePercent(int memory_usage_percent); - static UiElement getAEgo(float a_ego); - static UiElement getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit); - static UiElement getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid); - static UiElement getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid); - static UiElement getSteeringTorqueEps(float steering_torque_eps); - static UiElement getBearingDeg(float bearing_accuracy_deg, float bearing_deg); - static UiElement getAltitude(float gps_accuracy, float altitude); - static UiElement getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, - cereal::CarControl::Actuators::Reader &actuators, - float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override); -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h deleted file mode 100644 index 3711e5ac05..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h +++ /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. - */ -#pragma once - -#include - -struct UiElement { - QString value{}; - QString label{}; - QString units{}; - QColor color{}; - - explicit UiElement(const QString &value = "", const QString &label = "", const QString &units = "", const QColor &color = QColor(255, 255, 255, 255)) - : value(value), label(label), units(units), color(color) {} -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc deleted file mode 100644 index 2fc9eb98fc..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc +++ /dev/null @@ -1,887 +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 - -#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" - -#include "selfdrive/ui/qt/util.h" - - -HudRendererSP::HudRendererSP() { - plus_arrow_up_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_plus_arrow_up", {90, 90}); - minus_arrow_down_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_minus_arrow_down", {90, 90}); - - int size = e2e_alert_size * 2 - 40; - green_light_alert_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/green_light.png", {size, size}); - lead_depart_alert_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/lead_depart.png", {size, size}); -} - -void HudRendererSP::updateState(const UIState &s) { - HudRenderer::updateState(s); - - float speedConv = is_metric ? MS_TO_KPH : MS_TO_MPH; - devUiInfo = s.scene.dev_ui_info; - roadName = s.scene.road_name; - showTurnSignals = s.scene.turn_signals; - speedLimitMode = static_cast(s.scene.speed_limit_mode); - speedUnit = is_metric ? tr("km/h") : tr("mph"); - standstillTimer = s.scene.standstill_timer; - - const SubMaster &sm = *(s.sm); - const auto cs = sm["controlsState"].getControlsState(); - const auto car_state = sm["carState"].getCarState(); - const auto car_control = sm["carControl"].getCarControl(); - const auto radar_state = sm["radarState"].getRadarState(); - const auto is_gps_location_external = sm.rcv_frame("gpsLocationExternal") > 1; - const char *gps_source = is_gps_location_external ? "gpsLocationExternal" : "gpsLocation"; - const auto gpsLocation = is_gps_location_external ? sm[gps_source].getGpsLocationExternal() : sm[gps_source].getGpsLocation(); - const auto ltp = sm["liveTorqueParameters"].getLiveTorqueParameters(); - const auto car_params = sm["carParams"].getCarParams(); - const auto car_params_sp = sm["carParamsSP"].getCarParamsSP(); - const auto lp_sp = sm["longitudinalPlanSP"].getLongitudinalPlanSP(); - const auto lmd = sm["liveMapDataSP"].getLiveMapDataSP(); - - if (sm.updated("carParams")) { - steerControlType = car_params.getSteerControlType(); - } - - if (sm.updated("carParamsSP")) { - pcmCruiseSpeed = car_params_sp.getPcmCruiseSpeed(); - } - - if (sm.updated("longitudinalPlanSP")) { - speedLimit = lp_sp.getSpeedLimit().getResolver().getSpeedLimit() * speedConv; - speedLimitLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLast() * speedConv; - speedLimitOffset = lp_sp.getSpeedLimit().getResolver().getSpeedLimitOffset() * speedConv; - speedLimitValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitValid(); - speedLimitLastValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLastValid(); - speedLimitFinalLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitFinalLast() * speedConv; - speedLimitSource = lp_sp.getSpeedLimit().getResolver().getSource(); - speedLimitAssistState = lp_sp.getSpeedLimit().getAssist().getState(); - speedLimitAssistActive = lp_sp.getSpeedLimit().getAssist().getActive(); - smartCruiseControlVisionEnabled = lp_sp.getSmartCruiseControl().getVision().getEnabled(); - smartCruiseControlVisionActive = lp_sp.getSmartCruiseControl().getVision().getActive(); - smartCruiseControlMapEnabled = lp_sp.getSmartCruiseControl().getMap().getEnabled(); - smartCruiseControlMapActive = lp_sp.getSmartCruiseControl().getMap().getActive(); - } - greenLightAlert = lp_sp.getE2eAlerts().getGreenLightAlert(); - leadDepartAlert = lp_sp.getE2eAlerts().getLeadDepartAlert(); - - if (sm.updated("liveMapDataSP")) { - roadNameStr = QString::fromStdString(lmd.getRoadName()); - speedLimitAheadValid = lmd.getSpeedLimitAheadValid(); - speedLimitAhead = lmd.getSpeedLimitAhead() * speedConv; - speedLimitAheadDistance = lmd.getSpeedLimitAheadDistance(); - if (speedLimitAheadDistance < speedLimitAheadDistancePrev && speedLimitAheadValidFrame < SPEED_LIMIT_AHEAD_VALID_FRAME_THRESHOLD) { - speedLimitAheadValidFrame++; - } else if (speedLimitAheadDistance > speedLimitAheadDistancePrev && speedLimitAheadValidFrame > 0) { - speedLimitAheadValidFrame--; - } - } - speedLimitAheadDistancePrev = speedLimitAheadDistance; - - static int reverse_delay = 0; - bool reverse_allowed = false; - if (car_state.getGearShifter() != cereal::CarState::GearShifter::REVERSE) { - reverse_delay = 0; - reverse_allowed = false; - } else { - reverse_delay += 50; - if (reverse_delay >= 1000) { - reverse_allowed = true; - } - } - - reversing = reverse_allowed; - - if (sm.updated("liveParameters")) { - roll = sm["liveParameters"].getLiveParameters().getRoll(); - } - - if (sm.updated("deviceState")) { - memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent(); - } - - if (sm.updated(gps_source)) { - gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not. - altitude = gpsLocation.getAltitude(); - bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg(); - bearingDeg = gpsLocation.getBearingDeg(); - } - - if (sm.updated("liveTorqueParameters")) { - torquedUseParams = ltp.getUseParams(); - latAccelFactorFiltered = ltp.getLatAccelFactorFiltered(); - frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered(); - liveValid = ltp.getLiveValid(); - } - - latActive = car_control.getLatActive(); - actuators = car_control.getActuators(); - longOverride = car_control.getCruiseControl().getOverride(); - carControlEnabled = car_control.getEnabled(); - - steerOverride = car_state.getSteeringPressed(); - lead_d_rel = radar_state.getLeadOne().getDRel(); - lead_v_rel = radar_state.getLeadOne().getVRel(); - lead_status = radar_state.getLeadOne().getStatus(); - torqueLateral = steerControlType == cereal::CarParams::SteerControlType::TORQUE; - angleSteers = car_state.getSteeringAngleDeg(); - desiredCurvature = cs.getDesiredCurvature(); - curvature = cs.getCurvature(); - vEgo = car_state.getVEgo(); - aEgo = car_state.getAEgo(); - steeringTorqueEps = car_state.getSteeringTorqueEps(); - - isStandstill = car_state.getStandstill(); - if (!s.scene.started) standstillElapsedTime = 0.0; - - // override stock current speed values - float v_ego = (v_ego_cluster_seen && !s.scene.trueVEgoUI) ? car_state.getVEgoCluster() : car_state.getVEgo(); - speed = std::max(0.0f, v_ego * (is_metric ? MS_TO_KPH : MS_TO_MPH)); - hideVEgoUI = s.scene.hideVEgoUI; - - leftBlinkerOn = car_state.getLeftBlinker(); - rightBlinkerOn = car_state.getRightBlinker(); - leftBlindspot = car_state.getLeftBlindspot(); - rightBlindspot = car_state.getRightBlindspot(); - - speedCluster = car_state.getCruiseState().getSpeedCluster() * speedConv; - - allow_e2e_alerts = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE && - sm.rcv_frame("driverStateV2") > s.scene.started_frame && !reversing; -} - -void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { - HudRenderer::draw(p, surface_rect); - - e2eAlertDisplayTimer = std::max(0, e2eAlertDisplayTimer - 1); - - p.save(); - - if (is_cruise_available) { - drawSetSpeedSP(p, surface_rect); - } - - if (!hideVEgoUI) { - drawCurrentSpeedSP(p, surface_rect); - } - - if (!reversing) { - // Smart Cruise Control - int x_offset = -260; - int y1_offset = -80; - int y2_offset = -140; - - int y_scc_v = 0, y_scc_m = 0; - const int orders[2] = {y1_offset, y2_offset}; - int i = 0; - // SCC-V takes first order - if (smartCruiseControlVisionEnabled) y_scc_v = orders[i++]; - if (smartCruiseControlMapEnabled) y_scc_m = orders[i++]; - - // Smart Cruise Control - Vision - bool scc_vision_active_pulse = pulseElement(smartCruiseControlVisionFrame); - if ((smartCruiseControlVisionEnabled && !smartCruiseControlVisionActive) || (smartCruiseControlVisionActive && scc_vision_active_pulse)) { - drawSmartCruiseControlOnroadIcon(p, surface_rect, x_offset, y_scc_v, "SCC-V"); - } - smartCruiseControlVisionFrame = smartCruiseControlVisionActive ? (smartCruiseControlVisionFrame + 1) : 0; - - // Smart Cruise Control - Map - bool scc_map_active_pulse = pulseElement(smartCruiseControlMapFrame); - if ((smartCruiseControlMapEnabled && !smartCruiseControlMapActive) || (smartCruiseControlMapActive && scc_map_active_pulse)) { - drawSmartCruiseControlOnroadIcon(p, surface_rect, x_offset, y_scc_m, "SCC-M"); - } - smartCruiseControlMapFrame = smartCruiseControlMapActive ? (smartCruiseControlMapFrame + 1) : 0; - - // Bottom Dev UI - if (devUiInfo == 2) { - QRect rect_bottom(surface_rect.left(), surface_rect.bottom() - 60, surface_rect.width(), 61); - p.setPen(Qt::NoPen); - p.setBrush(QColor(0, 0, 0, 100)); - p.drawRect(rect_bottom); - drawBottomDevUI(p, rect_bottom.left(), rect_bottom.center().y()); - } - - // Right Dev UI - if (devUiInfo != 0) { - QRect rect_right(surface_rect.right() - (UI_BORDER_SIZE * 2), UI_BORDER_SIZE * 1.5, 184, 170); - drawRightDevUI(p, surface_rect.right() - 184 - UI_BORDER_SIZE * 2, UI_BORDER_SIZE * 2 + rect_right.height()); - } - - // Speed Limit - bool showSpeedLimit; - bool speed_limit_assist_pre_active_pulse = pulseElement(speedLimitAssistFrame); - - // Position speed limit sign next to set speed box - const int sign_width = is_metric ? 200 : 172; - const int sign_x = is_metric ? 280 : 272; - const int sign_y = 45; - const int sign_height = 204; - QRect sign_rect(sign_x, sign_y, sign_width, sign_height); - - if (speedLimitAssistState == cereal::LongitudinalPlanSP::SpeedLimit::AssistState::PRE_ACTIVE) { - speedLimitAssistFrame++; - showSpeedLimit = speed_limit_assist_pre_active_pulse; - drawSpeedLimitPreActiveArrow(p, sign_rect); - } else { - speedLimitAssistFrame = 0; - showSpeedLimit = speedLimitMode != SpeedLimitMode::OFF; - } - - if (showSpeedLimit) { - drawSpeedLimitSigns(p, sign_rect); - - // do not show during SLA's preActive state - if (speedLimitAssistState != cereal::LongitudinalPlanSP::SpeedLimit::AssistState::PRE_ACTIVE) { - drawUpcomingSpeedLimit(p); - } - } - - // Road Name - drawRoadName(p, surface_rect); - - // Green Light & Lead Depart Alerts - if (greenLightAlert || leadDepartAlert) { - e2eAlertDisplayTimer = 3 * UI_FREQ; - // reset onroad sleep timer for e2e alerts - uiStateSP()->reset_onroad_sleep_timer(); - } - - if (e2eAlertDisplayTimer > 0) { - e2eAlertFrame++; - if (greenLightAlert) { - alert_text = tr("GREEN\nLIGHT"); - alert_img = green_light_alert_img; - } - else if (leadDepartAlert) { - alert_text = tr("LEAD VEHICLE\nDEPARTING"); - alert_img = lead_depart_alert_img; - } - drawE2eAlert(p, surface_rect); - } - // Standstill Timer - else if (standstillTimer && isStandstill) { - alert_img = QPixmap(); - - standstillElapsedTime += 1.0 / UI_FREQ; - int minute = static_cast(standstillElapsedTime / 60); - int second = static_cast(standstillElapsedTime - (minute * 60)); - alert_text = QString("%1:%2").arg(minute, 1, 10, QChar('0')).arg(second, 2, 10, QChar('0')); - drawE2eAlert(p, surface_rect, tr("STOPPED")); - e2eAlertFrame++; - } - // No Alerts displayed - else { - e2eAlertFrame = 0; - if (!isStandstill) standstillElapsedTime = 0.0; - } - - // Blinker - if (showTurnSignals) { - drawBlinker(p, surface_rect); - } - } - - p.restore(); -} - -void HudRendererSP::drawText(QPainter &p, int x, int y, const QString &text, QColor color) { - QRect real_rect = p.fontMetrics().boundingRect(text); - real_rect.moveCenter({x, y - real_rect.height() / 2}); - p.setPen(color); - p.drawText(real_rect.x(), real_rect.bottom(), text); -} - -bool HudRendererSP::pulseElement(int frame) { - if (frame % UI_FREQ < (UI_FREQ / 2.5)) { - return false; - } - - return true; -} - -void HudRendererSP::drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &surface_rect, int x_offset, int y_offset, std::string name) { - int x = surface_rect.center().x(); - int y = surface_rect.height() / 4; - - QString text = QString::fromStdString(name); - QFont font = InterFont(36, QFont::Bold); - p.setFont(font); - - QFontMetrics fm(font); - - int padding_v = 5; - int box_width = 160; - int box_height = fm.height() + padding_v * 2; - - QRectF bg_rect(x - (box_width / 2) + x_offset, - y - (box_height / 2) + y_offset, - box_width, box_height); - - QPainterPath boxPath; - boxPath.addRoundedRect(bg_rect, 10, 10); - - int text_w = fm.horizontalAdvance(text); - qreal baseline_y = bg_rect.top() + padding_v + fm.ascent(); - qreal text_x = bg_rect.center().x() - (text_w / 2.0); - - QPainterPath textPath; - textPath.addText(QPointF(text_x, baseline_y), font, text); - boxPath = boxPath.subtracted(textPath); - - p.setPen(Qt::NoPen); - p.setBrush(longOverride ? QColor(0x91, 0x9b, 0x95, 0xf1) : QColor(0, 0xff, 0, 0xff)); - p.drawPath(boxPath); -} - -int HudRendererSP::drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { - - p.setFont(InterFont(28, QFont::Bold)); - x += 92; - y += 80; - drawText(p, x, y, label); - - p.setFont(InterFont(30 * 2, QFont::Bold)); - y += 65; - drawText(p, x, y, value, color); - - p.setFont(InterFont(28, QFont::Bold)); - - if (units.length() > 0) { - p.save(); - x += 120; - y -= 25; - p.translate(x, y); - p.rotate(-90); - drawText(p, 0, 0, units); - p.restore(); - } - - return 130; -} - -void HudRendererSP::drawRightDevUI(QPainter &p, int x, int y) { - int rh = 5; - int ry = y; - - UiElement dRelElement = DeveloperUi::getDRel(lead_status, lead_d_rel); - rh += drawRightDevUIElement(p, x, ry, dRelElement.value, dRelElement.label, dRelElement.units, dRelElement.color); - ry = y + rh; - - UiElement vRelElement = DeveloperUi::getVRel(lead_status, lead_v_rel, is_metric, speedUnit); - rh += drawRightDevUIElement(p, x, ry, vRelElement.value, vRelElement.label, vRelElement.units, vRelElement.color); - ry = y + rh; - - UiElement steeringAngleDegElement = DeveloperUi::getSteeringAngleDeg(angleSteers, latActive, steerOverride); - rh += drawRightDevUIElement(p, x, ry, steeringAngleDegElement.value, steeringAngleDegElement.label, steeringAngleDegElement.units, steeringAngleDegElement.color); - ry = y + rh; - - UiElement actuatorsOutputLateralElement = DeveloperUi::getActuatorsOutputLateral(steerControlType, actuators, desiredCurvature, vEgo, roll, latActive, steerOverride); - rh += drawRightDevUIElement(p, x, ry, actuatorsOutputLateralElement.value, actuatorsOutputLateralElement.label, actuatorsOutputLateralElement.units, actuatorsOutputLateralElement.color); - ry = y + rh; - - UiElement actualLateralAccelElement = DeveloperUi::getActualLateralAccel(curvature, vEgo, roll, latActive, steerOverride); - rh += drawRightDevUIElement(p, x, ry, actualLateralAccelElement.value, actualLateralAccelElement.label, actualLateralAccelElement.units, actualLateralAccelElement.color); -} - -int HudRendererSP::drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { - p.setFont(InterFont(38, QFont::Bold)); - QFontMetrics fm(p.font()); - QRect init_rect = fm.boundingRect(label + " "); - QRect real_rect = fm.boundingRect(init_rect, 0, label + " "); - real_rect.moveCenter({x, y}); - - QRect init_rect2 = fm.boundingRect(value); - QRect real_rect2 = fm.boundingRect(init_rect2, 0, value); - real_rect2.moveTop(real_rect.top()); - real_rect2.moveLeft(real_rect.right() + 10); - - QRect init_rect3 = fm.boundingRect(units); - QRect real_rect3 = fm.boundingRect(init_rect3, 0, units); - real_rect3.moveTop(real_rect.top()); - real_rect3.moveLeft(real_rect2.right() + 10); - - p.setPen(Qt::white); - p.drawText(real_rect, Qt::AlignLeft | Qt::AlignVCenter, label); - - p.setPen(color); - p.drawText(real_rect2, Qt::AlignRight | Qt::AlignVCenter, value); - p.drawText(real_rect3, Qt::AlignLeft | Qt::AlignVCenter, units); - return 430; -} - -void HudRendererSP::drawBottomDevUI(QPainter &p, int x, int y) { - int rw = 90; - - UiElement aEgoElement = DeveloperUi::getAEgo(aEgo); - rw += drawBottomDevUIElement(p, rw, y, aEgoElement.value, aEgoElement.label, aEgoElement.units, aEgoElement.color); - - UiElement vEgoLeadElement = DeveloperUi::getVEgoLead(lead_status, lead_v_rel, vEgo, is_metric, speedUnit); - rw += drawBottomDevUIElement(p, rw, y, vEgoLeadElement.value, vEgoLeadElement.label, vEgoLeadElement.units, vEgoLeadElement.color); - - if (torqueLateral && torquedUseParams) { - UiElement frictionCoefficientFilteredElement = DeveloperUi::getFrictionCoefficientFiltered(frictionCoefficientFiltered, liveValid); - rw += drawBottomDevUIElement(p, rw, y, frictionCoefficientFilteredElement.value, frictionCoefficientFilteredElement.label, frictionCoefficientFilteredElement.units, frictionCoefficientFilteredElement.color); - - UiElement latAccelFactorFilteredElement = DeveloperUi::getLatAccelFactorFiltered(latAccelFactorFiltered, liveValid); - rw += drawBottomDevUIElement(p, rw, y, latAccelFactorFilteredElement.value, latAccelFactorFilteredElement.label, latAccelFactorFilteredElement.units, latAccelFactorFilteredElement.color); - } else { - UiElement steeringTorqueEpsElement = DeveloperUi::getSteeringTorqueEps(steeringTorqueEps); - rw += drawBottomDevUIElement(p, rw, y, steeringTorqueEpsElement.value, steeringTorqueEpsElement.label, steeringTorqueEpsElement.units, steeringTorqueEpsElement.color); - - UiElement bearingDegElement = DeveloperUi::getBearingDeg(bearingAccuracyDeg, bearingDeg); - rw += drawBottomDevUIElement(p, rw, y, bearingDegElement.value, bearingDegElement.label, bearingDegElement.units, bearingDegElement.color); - } - - UiElement altitudeElement = DeveloperUi::getAltitude(gpsAccuracy, altitude); - rw += drawBottomDevUIElement(p, rw, y, altitudeElement.value, altitudeElement.label, altitudeElement.units, altitudeElement.color); -} - -void HudRendererSP::drawSpeedLimitSigns(QPainter &p, QRect &sign_rect) { - bool speedLimitWarningEnabled = speedLimitMode >= SpeedLimitMode::WARNING; // TODO-SP: update to include SpeedLimitMode::ASSIST - bool hasSpeedLimit = speedLimitValid || speedLimitLastValid; - bool overspeed = hasSpeedLimit && std::nearbyint(speedLimitFinalLast) < std::nearbyint(speed); - QString speedLimitStr = hasSpeedLimit ? QString::number(std::nearbyint(speedLimitLast)) : "---"; - - // Offset display text - QString speedLimitSubText = ""; - if (speedLimitOffset != 0) { - speedLimitSubText = (speedLimitOffset > 0 ? "" : "-") + QString::number(std::nearbyint(speedLimitOffset)); - } - - float speedLimitSubTextFactor = is_metric ? 0.5 : 0.6; - if (speedLimitSubText.size() >= 3) { - speedLimitSubTextFactor = 0.475; - } - - int alpha = 255; - QColor red_color = QColor(255, 0, 0, alpha); - QColor speed_color = (speedLimitWarningEnabled && overspeed) ? red_color : - (!speedLimitValid && speedLimitLastValid ? QColor(0x91, 0x9b, 0x95, 0xf1) : QColor(0, 0, 0, alpha)); - - if (is_metric) { - // EU Vienna Convention style circular sign - QRect vienna_rect = sign_rect; - int circle_size = std::min(vienna_rect.width(), vienna_rect.height()); - QRect circle_rect(vienna_rect.x(), vienna_rect.y(), circle_size, circle_size); - - if (vienna_rect.width() > vienna_rect.height()) { - circle_rect.moveLeft(vienna_rect.x() + (vienna_rect.width() - circle_size) / 2); - } else if (vienna_rect.height() > vienna_rect.width()) { - circle_rect.moveTop(vienna_rect.y() + (vienna_rect.height() - circle_size) / 2); - } - - // White background circle - p.setPen(Qt::NoPen); - p.setBrush(QColor(255, 255, 255, alpha)); - p.drawEllipse(circle_rect); - - // Red border ring with color coding - QRect red_ring = circle_rect; - - p.setBrush(red_color); - p.drawEllipse(red_ring); - - // Center white circle for text - int ring_size = circle_size * 0.12; - QRect center_circle = red_ring.adjusted(ring_size, ring_size, -ring_size, -ring_size); - p.setBrush(QColor(255, 255, 255, alpha)); - p.drawEllipse(center_circle); - - // Speed value, smaller font for 3+ digits - int font_size = (speedLimitStr.size() >= 3) ? 70 : 85; - p.setFont(InterFont(font_size, QFont::Bold)); - - p.setPen(speed_color); - p.drawText(center_circle, Qt::AlignCenter, speedLimitStr); - - // Offset value in small circular box - if (!speedLimitSubText.isEmpty() && hasSpeedLimit) { - int offset_circle_size = circle_size * 0.4; - int overlap = offset_circle_size * 0.25; - QRect offset_circle_rect( - circle_rect.right() - offset_circle_size/1.25 + overlap, - circle_rect.top() - offset_circle_size/1.75 + overlap, - offset_circle_size, - offset_circle_size - ); - - p.setPen(QPen(QColor(77, 77, 77, 255), 6)); - p.setBrush(QColor(0, 0, 0, alpha)); - p.drawEllipse(offset_circle_rect); - - p.setFont(InterFont(offset_circle_size * speedLimitSubTextFactor, QFont::Bold)); - p.setPen(QColor(255, 255, 255, alpha)); - p.drawText(offset_circle_rect, Qt::AlignCenter, speedLimitSubText); - } - } else { - // US/Canada MUTCD style sign - p.setPen(Qt::NoPen); - p.setBrush(QColor(255, 255, 255, alpha)); - p.drawRoundedRect(sign_rect, 32, 32); - - // Inner border with violation color coding - QRect inner_rect = sign_rect.adjusted(10, 10, -10, -10); - QColor border_color = QColor(0, 0, 0, alpha); - - p.setPen(QPen(border_color, 4)); - p.setBrush(QColor(255, 255, 255, alpha)); - p.drawRoundedRect(inner_rect, 22, 22); - - // "SPEED LIMIT" text - p.setFont(InterFont(40, QFont::DemiBold)); - p.setPen(QColor(0, 0, 0, alpha)); - p.drawText(inner_rect.adjusted(0, 10, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("SPEED")); - p.drawText(inner_rect.adjusted(0, 50, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("LIMIT")); - - // Speed value with color coding - p.setFont(InterFont(90, QFont::Bold)); - - p.setPen(speed_color); - p.drawText(inner_rect.adjusted(0, 80, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitStr); - - // Offset value in small box - if (!speedLimitSubText.isEmpty() && hasSpeedLimit) { - int offset_box_size = sign_rect.width() * 0.4; - int overlap = offset_box_size * 0.25; - QRect offset_box_rect( - sign_rect.right() - offset_box_size/1.5 + overlap, - sign_rect.top() - offset_box_size/1.25 + overlap, - offset_box_size, - offset_box_size - ); - - int corner_radius = offset_box_size * 0.2; - p.setPen(QPen(QColor(77, 77, 77, 255), 6)); - p.setBrush(QColor(0, 0, 0, alpha)); - p.drawRoundedRect(offset_box_rect, corner_radius, corner_radius); - - p.setFont(InterFont(offset_box_size * speedLimitSubTextFactor, QFont::Bold)); - p.setPen(QColor(255, 255, 255, alpha)); - p.drawText(offset_box_rect, Qt::AlignCenter, speedLimitSubText); - } - } -} - -void HudRendererSP::drawUpcomingSpeedLimit(QPainter &p) { - bool speed_limit_ahead = speedLimitAheadValid && speedLimitAhead > 0 && speedLimitAhead != speedLimit && speedLimitAheadValidFrame > 0 && - speedLimitSource == cereal::LongitudinalPlanSP::SpeedLimit::Source::MAP; - if (!speed_limit_ahead) { - return; - } - - auto roundToInterval = [&](float distance, int interval, int threshold) { - int base = static_cast(distance / interval) * interval; - return (distance - base >= threshold) ? base + interval : base; - }; - - auto outputDistance = [&] { - if (is_metric) { - if (speedLimitAheadDistance < 50) return tr("Near"); - if (speedLimitAheadDistance >= 1000) return QString::number(speedLimitAheadDistance * METER_TO_KM, 'f', 1) + tr("km"); - - int rounded = (speedLimitAheadDistance < 200) ? std::max(10, roundToInterval(speedLimitAheadDistance, 10, 5)) : roundToInterval(speedLimitAheadDistance, 100, 50); - return QString::number(rounded) + tr("m"); - } else { - float distance_ft = speedLimitAheadDistance * METER_TO_FOOT; - if (distance_ft < 100) return tr("Near"); - if (distance_ft >= 900) return QString::number(speedLimitAheadDistance * METER_TO_MILE, 'f', 1) + tr("mi"); - - int rounded = (distance_ft < 500) ? std::max(50, roundToInterval(distance_ft, 50, 25)) : roundToInterval(distance_ft, 100, 50); - return QString::number(rounded) + tr("ft"); - } - }; - - QString speedStr = QString::number(std::nearbyint(speedLimitAhead)); - QString distanceStr = outputDistance(); - - // Position below current speed limit sign - const int sign_width = is_metric ? 200 : 172; - const int sign_x = is_metric ? 280 : 272; - const int sign_y = 45; - const int sign_height = 204; - - const int ahead_width = 170; - const int ahead_height = 160; - const int ahead_x = sign_x + (sign_width - ahead_width) / 2; - const int ahead_y = sign_y + sign_height + 10; - - QRect ahead_rect(ahead_x, ahead_y, ahead_width, ahead_height); - p.setPen(QPen(QColor(255, 255, 255, 100), 3)); - p.setBrush(QColor(0, 0, 0, 180)); - p.drawRoundedRect(ahead_rect, 16, 16); - - // "AHEAD" label - p.setFont(InterFont(40, QFont::DemiBold)); - p.setPen(QColor(200, 200, 200, 255)); - p.drawText(ahead_rect.adjusted(0, 4, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("AHEAD")); - - // Speed value - p.setFont(InterFont(70, QFont::Bold)); - p.setPen(QColor(255, 255, 255, 255)); - p.drawText(ahead_rect.adjusted(0, 38, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedStr); - - // Distance - p.setFont(InterFont(40, QFont::Normal)); - p.setPen(QColor(180, 180, 180, 255)); - p.drawText(ahead_rect.adjusted(0, 110, 0, 0), Qt::AlignTop | Qt::AlignHCenter, distanceStr); -} - -void HudRendererSP::drawRoadName(QPainter &p, const QRect &surface_rect) { - if (!roadName || roadNameStr.isEmpty()) return; - - // Measure text to size container - p.setFont(InterFont(46, QFont::DemiBold)); - QFontMetrics fm(p.font()); - - int text_width = fm.horizontalAdvance(roadNameStr); - int padding = 40; - int rect_width = text_width + padding; - - // Constrain to reasonable bounds - int min_width = 200; - int max_width = surface_rect.width() - 40; - rect_width = std::max(min_width, std::min(rect_width, max_width)); - - // Center at top of screen - QRect road_rect(surface_rect.width() / 2 - rect_width / 2, -4, rect_width, 60); - - p.setPen(Qt::NoPen); - p.setBrush(QColor(0, 0, 0, 120)); - p.drawRoundedRect(road_rect, 12, 12); - - p.setPen(QColor(255, 255, 255, 200)); - - // Truncate if still too long - QString truncated = fm.elidedText(roadNameStr, Qt::ElideRight, road_rect.width() - 20); - p.drawText(road_rect, Qt::AlignCenter, truncated); -} - -void HudRendererSP::drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect) { - const int sign_margin = 12; - const int arrow_spacing = sign_margin * 1.4; - int arrow_x = sign_rect.right() + arrow_spacing; - - int _set_speed = std::nearbyint(set_speed); - int _speed_limit_final_last = std::nearbyint(speedLimitFinalLast); - - // Calculate the vertical offset using a sinusoidal function for smooth bouncing - double bounce_frequency = 2.0 * M_PI / UI_FREQ; // 20 frames for one full oscillation - int bounce_offset = 20 * sin(speedLimitAssistFrame * bounce_frequency); // Adjust the amplitude (20 pixels) as needed - - if (_set_speed < _speed_limit_final_last) { - QPoint iconPosition(arrow_x, sign_rect.center().y() - plus_arrow_up_img.height() / 2 + bounce_offset); - p.drawPixmap(iconPosition, plus_arrow_up_img); - } else if (_set_speed > _speed_limit_final_last) { - QPoint iconPosition(arrow_x, sign_rect.center().y() - minus_arrow_down_img.height() / 2 - bounce_offset); - p.drawPixmap(iconPosition, minus_arrow_down_img); - } -} - -void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) { - // Draw outer box + border to contain set speed - const QSize default_size = {172, 204}; - QSize set_speed_size = is_metric ? QSize(200, 204) : default_size; - QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size); - - // Draw set speed box - p.setPen(QPen(QColor(255, 255, 255, 75), 6)); - p.setBrush(QColor(0, 0, 0, 166)); - p.drawRoundedRect(set_speed_rect, 32, 32); - - // Colors based on status - QColor max_color = QColor(0xa6, 0xa6, 0xa6, 0xff); - QColor set_speed_color = QColor(0x72, 0x72, 0x72, 0xff); - if (is_cruise_set) { - set_speed_color = QColor(255, 255, 255); - if (speedLimitAssistActive) { - set_speed_color = longOverride ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 0xff, 0, 0xff); - max_color = longOverride ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0x80, 0xd8, 0xa6, 0xff); - } else { - if (status == STATUS_DISENGAGED) { - max_color = QColor(255, 255, 255); - } else if (status == STATUS_OVERRIDE) { - max_color = QColor(0x91, 0x9b, 0x95, 0xff); - } else { - max_color = QColor(0x80, 0xd8, 0xa6, 0xff); - } - } - } - - // Draw "MAX" or carState.cruiseState.speedCluster (when ICBM is active) text - if (!pcmCruiseSpeed && carControlEnabled) { - if (std::nearbyint(set_speed) != std::nearbyint(speedCluster)) { - icbm_active_counter = 3 * UI_FREQ; - } else if (icbm_active_counter > 0) { - icbm_active_counter--; - } - } else { - icbm_active_counter = 0; - } - int max_str_size = (icbm_active_counter != 0) ? 60 : 40; - int max_str_y = (icbm_active_counter != 0) ? 15 : 27; - QString max_str = (icbm_active_counter != 0) ? QString::number(std::nearbyint(speedCluster)) : tr("MAX"); - - p.setFont(InterFont(max_str_size, QFont::DemiBold)); - p.setPen(max_color); - p.drawText(set_speed_rect.adjusted(0, max_str_y, 0, 0), Qt::AlignTop | Qt::AlignHCenter, max_str); - - // Draw set speed - QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "–"; - p.setFont(InterFont(90, QFont::Bold)); - p.setPen(set_speed_color); - p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr); -} - -void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text) { - if (!allow_e2e_alerts) return; - - int x = surface_rect.right() - e2e_alert_size - (devUiInfo > 0 ? 180 : 100) - (UI_BORDER_SIZE * 3); - int y = surface_rect.center().y() + 20; - QRect alertRect(x - e2e_alert_size, y - e2e_alert_size, e2e_alert_size * 2, e2e_alert_size * 2); - - // Alert Circle - QPoint center = alertRect.center(); - QColor frameColor; - if (!alert_alt_text.isEmpty()) frameColor = QColor(255, 255, 255, 75); - else frameColor = pulseElement(e2eAlertFrame) ? QColor(255, 255, 255, 75) : QColor(0, 255, 0, 75); - p.setPen(QPen(frameColor, 15)); - p.setBrush(QColor(0, 0, 0, 190)); - p.drawEllipse(center, e2e_alert_size, e2e_alert_size); - - // Alert Text - QColor txtColor; - QFont font; - int alert_bottom_adjustment; - if (!alert_alt_text.isEmpty()) { - font = InterFont(100, QFont::Bold); - alert_bottom_adjustment = 5; - txtColor = QColor(255, 255, 255, 255); - } else { - font = InterFont(48, QFont::Bold); - alert_bottom_adjustment = 7; - txtColor = pulseElement(e2eAlertFrame) ? QColor(255, 255, 255, 255) : QColor(0, 255, 0, 190); - } - p.setPen(txtColor); - p.setFont(font); - QFontMetrics fm(p.font()); - QRect textRect = fm.boundingRect(alertRect, Qt::TextWordWrap, alert_text); - textRect.moveCenter({alertRect.center().x(), alertRect.center().y()}); - textRect.moveBottom(alertRect.bottom() - alertRect.height() / alert_bottom_adjustment); - p.drawText(textRect, Qt::AlignCenter, alert_text); - - if (!alert_alt_text.isEmpty()) { - // Alert Alternate Text - p.setFont(InterFont(80, QFont::Bold)); - p.setPen(QColor(255, 175, 3, 240)); - QFontMetrics fmt(p.font()); - QRect topTextRect = fmt.boundingRect(alertRect, Qt::TextWordWrap, alert_alt_text); - topTextRect.moveCenter({alertRect.center().x(), alertRect.center().y()}); - topTextRect.moveTop(alertRect.top() + alertRect.height() / 3.5); - p.drawText(topTextRect, Qt::AlignCenter, alert_alt_text); - } else { - // Alert Image instead of Top Text - QPointF pixmapCenterOffset = QPointF(alert_img.width() / 2.0, alert_img.height() / 2.0); - QPointF drawPoint = center - pixmapCenterOffset; - p.drawPixmap(drawPoint, alert_img); - } -} - -void HudRendererSP::drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect) { - QString speedStr = QString::number(std::nearbyint(speed)); - - p.setFont(InterFont(176, QFont::Bold)); - HudRenderer::drawText(p, surface_rect.center().x(), 210, speedStr); - - p.setFont(InterFont(66)); - HudRenderer::drawText(p, surface_rect.center().x(), 290, is_metric ? tr("km/h") : tr("mph"), 200); -} - -void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) { - const bool hazard = leftBlinkerOn && rightBlinkerOn; - int blinkerStatus = hazard ? 2 : (leftBlinkerOn || rightBlinkerOn) ? 1 : 0; - - if (!leftBlinkerOn && !rightBlinkerOn) { - blinkerFrameCounter = 0; - lastBlinkerStatus = 0; - return; - } - - if (blinkerStatus != lastBlinkerStatus) { - blinkerFrameCounter = 0; - lastBlinkerStatus = blinkerStatus; - } - - ++blinkerFrameCounter; - - const int BLINKER_COOLDOWN_FRAMES = UI_FREQ / 10; - if (blinkerFrameCounter < BLINKER_COOLDOWN_FRAMES) { - return; - } - - const int circleRadius = 60; - const int arrowLength = 60; - const int x_gap = 160; - const int y_offset = 272; - - const int centerX = surface_rect.center().x(); - - const QPen bgBorder(Qt::white, 5); - const QPen arrowPen(Qt::NoPen); - - p.save(); - - auto drawArrow = [&](int cx, int cy, int dir, const QBrush &arrowBrush) { - const int bodyLength = arrowLength / 2; - const int bodyWidth = arrowLength / 2; - const int headLength = arrowLength / 2; - const int headWidth = arrowLength; - - QPolygon arrow; - arrow.reserve(7); - arrow << QPoint(cx - dir * bodyLength, cy - bodyWidth / 2) - << QPoint(cx, cy - bodyWidth / 2) - << QPoint(cx, cy - headWidth / 2) - << QPoint(cx + dir * headLength, cy) - << QPoint(cx, cy + headWidth / 2) - << QPoint(cx, cy + bodyWidth / 2) - << QPoint(cx - dir * bodyLength, cy + bodyWidth / 2); - - p.setPen(arrowPen); - p.setBrush(arrowBrush); - p.drawPolygon(arrow); - }; - - auto drawCircle = [&](int cx, int cy, const QBrush &bgBrush) { - p.setPen(bgBorder); - p.setBrush(bgBrush); - p.drawEllipse(QPoint(cx, cy), circleRadius, circleRadius); - }; - - struct BlinkerSide { bool on; int dir; bool blocked; int cx; }; - const std::array sides = {{ - {leftBlinkerOn, -1, hazard ? true : (leftBlinkerOn && leftBlindspot), centerX - x_gap}, - {rightBlinkerOn, 1, hazard ? true : (rightBlinkerOn && rightBlindspot), centerX + x_gap}, - }}; - - for (const auto &s: sides) { - if (!s.on) continue; - - QColor bgColor = s.blocked ? QColor(135, 23, 23) : QColor(23, 134, 68); - QColor arrowColor = s.blocked ? QColor(66, 12, 12) : QColor(12, 67, 34); - if (pulseElement(blinkerFrameCounter)) arrowColor = Qt::white; - - const QBrush bgBrush(bgColor); - const QBrush arrowBrush(arrowColor); - - drawCircle(s.cx, y_offset, bgBrush); - drawArrow(s.cx, y_offset, s.dir, arrowBrush); - } - - p.restore(); -} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h deleted file mode 100644 index f021a949e4..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.h +++ /dev/null @@ -1,123 +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/hud.h" -#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/helpers.h" -#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" - -constexpr int SPEED_LIMIT_AHEAD_VALID_FRAME_THRESHOLD = 5; - -class HudRendererSP : public HudRenderer { - Q_OBJECT - -public: - HudRendererSP(); - void updateState(const UIState &s) override; - void draw(QPainter &p, const QRect &surface_rect) override; - -private: - Params params; - void drawText(QPainter &p, int x, int y, const QString &text, QColor color = Qt::white); - void drawRightDevUI(QPainter &p, int x, int y); - int drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); - int drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); - void drawBottomDevUI(QPainter &p, int x, int y); - void drawStandstillTimer(QPainter &p, int x, int y); - bool pulseElement(int frame); - void drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &surface_rect, int x_offset, int y_offset, std::string name); - void drawSpeedLimitSigns(QPainter &p, QRect &sign_rect); - void drawUpcomingSpeedLimit(QPainter &p); - void drawRoadName(QPainter &p, const QRect &surface_rect); - void drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect); - void drawSetSpeedSP(QPainter &p, const QRect &surface_rect); - void drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text = ""); - void drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect); - void drawBlinker(QPainter &p, const QRect &surface_rect); - - bool lead_status; - float lead_d_rel; - float lead_v_rel; - bool torqueLateral; - float angleSteers; - float desiredCurvature; - float curvature; - float roll; - int memoryUsagePercent; - int devUiInfo; - float gpsAccuracy; - float altitude; - float vEgo; - float aEgo; - float steeringTorqueEps; - float bearingAccuracyDeg; - float bearingDeg; - bool torquedUseParams; - float latAccelFactorFiltered; - float frictionCoefficientFiltered; - bool liveValid; - QString speedUnit; - bool latActive; - bool steerOverride; - bool reversing; - cereal::CarParams::SteerControlType steerControlType; - cereal::CarControl::Actuators::Reader actuators; - bool standstillTimer; - bool isStandstill; - float standstillElapsedTime; - bool longOverride; - bool smartCruiseControlVisionEnabled; - bool smartCruiseControlVisionActive; - int smartCruiseControlVisionFrame; - bool smartCruiseControlMapEnabled; - bool smartCruiseControlMapActive; - int smartCruiseControlMapFrame; - float speedLimit; - float speedLimitLast; - float speedLimitOffset; - bool speedLimitValid; - bool speedLimitLastValid; - float speedLimitFinalLast; - cereal::LongitudinalPlanSP::SpeedLimit::Source speedLimitSource; - bool speedLimitAheadValid; - float speedLimitAhead; - float speedLimitAheadDistance; - float speedLimitAheadDistancePrev; - int speedLimitAheadValidFrame; - SpeedLimitMode speedLimitMode = SpeedLimitMode::OFF; - bool roadName; - QString roadNameStr; - cereal::LongitudinalPlanSP::SpeedLimit::AssistState speedLimitAssistState; - bool speedLimitAssistActive; - int speedLimitAssistFrame; - QPixmap plus_arrow_up_img; - QPixmap minus_arrow_down_img; - int e2e_alert_size = 250; - QPixmap green_light_alert_img; - bool greenLightAlert; - int e2eAlertFrame; - int e2eAlertDisplayTimer = 0; - bool allow_e2e_alerts; - bool leadDepartAlert; - QPixmap lead_depart_alert_img; - QString alert_text; - QPixmap alert_img; - bool hideVEgoUI; - bool leftBlinkerOn; - bool rightBlinkerOn; - bool leftBlindspot; - bool rightBlindspot; - int blinkerFrameCounter; - int lastBlinkerStatus; - bool showTurnSignals; - - bool carControlEnabled; - float speedCluster = 0; - int icbm_active_counter = 0; - bool pcmCruiseSpeed = true; -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.cc b/selfdrive/ui/sunnypilot/qt/onroad/model.cc deleted file mode 100644 index 590ade24a1..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.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 "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::draw(QPainter &painter, const QRect &surface_rect) { - auto *s = uiState(); - auto &sm = *(s->sm); - - if (sm.rcv_frame("liveCalibration") < s->scene.started_frame || - sm.rcv_frame("modelV2") < s->scene.started_frame) { - return; - } - - clip_region = surface_rect.adjusted(-CLIP_MARGIN, -CLIP_MARGIN, CLIP_MARGIN, CLIP_MARGIN); - experimental_mode = sm["selfdriveState"].getSelfdriveState().getExperimentalMode(); - longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl(); - path_offset_z = sm["liveCalibration"].getLiveCalibration().getHeight()[0]; - - painter.save(); - - const auto &model = sm["modelV2"].getModelV2(); - const auto &radar_state = sm["radarState"].getRadarState(); - const auto &lead_one = radar_state.getLeadOne(); - const auto &car_state = sm["carState"].getCarState(); - - update_model(model, lead_one); - drawLaneLines(painter); - - if (s->scene.rainbow_mode) { - drawRainbowPath(painter, surface_rect); - } else { - ModelRenderer::drawPath(painter, model, surface_rect.height()); - } - - if (longitudinal_control && sm.alive("radarState")) { - update_leads(radar_state, model.getPosition()); - const auto &lead_two = radar_state.getLeadTwo(); - if (lead_one.getStatus()) { - drawLead(painter, lead_one, lead_vertices[0], surface_rect); - } - if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) { - drawLead(painter, lead_two, lead_vertices[1], surface_rect); - } - } - - if (s->scene.blindspot_ui) { - const bool left_blindspot = car_state.getLeftBlindspot(); - const bool right_blindspot = car_state.getRightBlindspot(); - drawBlindspot(painter, surface_rect, left_blindspot, right_blindspot); - } - drawLeadStatus(painter, surface_rect.height(), surface_rect.width()); - - painter.restore(); -} - -void ModelRendererSP::drawBlindspot(QPainter &painter, const QRect &surface_rect, bool left_blindspot, bool right_blindspot) { - 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); - } -} - -void ModelRendererSP::drawLeadStatus(QPainter &painter, int height, int width) { - auto *s = uiState(); - auto &sm = *(s->sm); - - bool longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl(); - if (!longitudinal_control) { - lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f); - return; - } - - if (!sm.alive("radarState")) { - lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f); - return; - } - - const auto &radar_state = sm["radarState"].getRadarState(); - const auto &lead_one = radar_state.getLeadOne(); - const auto &lead_two = radar_state.getLeadTwo(); - - bool has_lead_one = lead_one.getStatus(); - bool has_lead_two = lead_two.getStatus(); - - if (!has_lead_one && !has_lead_two) { - lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f); - if (lead_status_alpha <= 0.0f) return; - } else { - lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f); - } - - if (has_lead_one) { - drawLeadStatusPosition(painter, lead_one, lead_vertices[0], height, width); - } - - if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) { - drawLeadStatusPosition(painter, lead_two, lead_vertices[1], height, width); - } -} - -void ModelRendererSP::drawLeadStatusPosition(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, - const QPointF &chevron_pos, int height, int width) { - float d_rel = lead_data.getDRel(); - float v_rel = lead_data.getVRel(); - auto *s = uiState(); - auto &sm = *(s->sm); - float v_ego = sm["carState"].getCarState().getVEgo(); - - int chevron_data = s->scene.chevron_info; - float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35; - - QFont content_font = painter.font(); - content_font.setPixelSize(50); - content_font.setBold(true); - painter.setFont(content_font); - - bool is_metric = s->scene.is_metric; - QStringList text_lines; - const int chevron_all = 4; - QStringList chevron_text[3]; - - // Distance display - if (chevron_data == 1 || chevron_data == chevron_all) { - int pos = 0; - float val = std::max(0.0f, d_rel); - QString unit = is_metric ? "m" : "ft"; - if (!is_metric) val *= 3.28084f; - chevron_text[pos].append(QString::number(val, 'f', 0) + " " + unit); - } - - // Speed display - if (chevron_data == 2 || chevron_data == chevron_all) { - int pos = (chevron_data == 2) ? 0 : 1; - float multiplier = is_metric ? static_cast(MS_TO_KPH) : static_cast(MS_TO_MPH); - float val = std::max(0.0f, (v_rel + v_ego) * multiplier); - QString unit = is_metric ? "km/h" : "mph"; - chevron_text[pos].append(QString::number(val, 'f', 0) + " " + unit); - } - - // Time to contact - if (chevron_data == 3 || chevron_data == chevron_all) { - int pos = (chevron_data == 3) ? 0 : 2; - float val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f; - QString ttc = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---"; - chevron_text[pos].append(ttc); - } - - for (int i = 0; i < 3; ++i) { - if (!chevron_text[i].isEmpty()) text_lines.append(chevron_text[i]); - } - - if (text_lines.isEmpty()) return; - - QFontMetrics fm(content_font); - float text_width = 120.0f; - for (const QString &line : text_lines) { - text_width = std::max(text_width, fm.horizontalAdvance(line) + 20.0f); - } - text_width = std::min(text_width, 250.0f); - - float line_height = 50.0f; - float total_height = text_lines.size() * line_height; - float margin = 20.0f; - - float text_y = chevron_pos.y() + sz + 15; - if (text_y + total_height > height - margin) { - float y_max = chevron_pos.y() > (height - margin) ? (height - margin) : chevron_pos.y(); - text_y = y_max - 15 - total_height; - text_y = std::max(margin, text_y); - } - - float text_x = chevron_pos.x() - text_width / 2; - text_x = std::clamp(text_x, margin, (float)width - text_width - margin); - - QPoint shadow_offset(2, 2); - QColor text_color = QColor(255, 255, 255, (int)(255 * lead_status_alpha)); - for (int i = 0; i < text_lines.size(); ++i) { - float y = text_y + (i * line_height); - if (y + line_height > height - margin) break; - - QRect rect(text_x, y, text_width, line_height); - - // Draw shadow - painter.setPen(QColor(0, 0, 0, (int)(200 * lead_status_alpha))); - painter.drawText(rect.translated(shadow_offset), Qt::AlignCenter, text_lines[i]); - painter.setPen(text_color); - painter.drawText(rect, Qt::AlignCenter, text_lines[i]); - } - - painter.setPen(Qt::NoPen); -} - -void ModelRendererSP::drawRainbowPath(QPainter &painter, const QRect &surface_rect) { - // Simple time-based animation - float time_offset = std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count() / 1000.0f; - - // simple linear gradient from bottom to top - QLinearGradient bg(0, surface_rect.height(), 0, 0); - - // evenly spaced colors across the spectrum - // The animation shifts the entire spectrum smoothly - float animation_speed = 40.0f; // speed vroom vroom - float hue_offset = fmod(time_offset * animation_speed, 360.0f); - - // 6-8 color stops for smooth transitions more color makes it laggy - const int num_stops = 7; - for (int i = 0; i < num_stops; i++) { - float position = static_cast(i) / (num_stops - 1); - - float hue = fmod(hue_offset + position * 360.0f, 360.0f); - float saturation = 0.9f; - float lightness = 0.6f; - - // Alpha fades out towards the far end of the path - float alpha = 0.8f * (1.0f - position * 0.3f); - - QColor color = QColor::fromHslF(hue / 360.0f, saturation, lightness, alpha); - bg.setColorAt(position, color); - } - - painter.setBrush(bg); - painter.drawPolygon(track_vertices); -} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.h b/selfdrive/ui/sunnypilot/qt/onroad/model.h deleted file mode 100644 index 68068f2068..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.h +++ /dev/null @@ -1,31 +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/model.h" - -class ModelRendererSP : public ModelRenderer { -public: - ModelRendererSP() = default; - - void draw(QPainter &painter, const QRect &surface_rect); - -private: - void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) override; - void drawLeadStatus(QPainter &painter, int height, int width); - void drawLeadStatusPosition(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, - const QPointF &chevron_pos, int height, int width); - void drawBlindspot(QPainter &painter, const QRect &surface_rect, bool left_blindspot, bool right_blindspot); - void drawRainbowPath(QPainter &painter, const QRect &surface_rect); - - QPolygonF left_blindspot_vertices; - QPolygonF right_blindspot_vertices; - - // Lead status animation - float lead_status_alpha = 0.0f; -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.cc b/selfdrive/ui/sunnypilot/qt/onroad/onroad_home.cc deleted file mode 100644 index b26d1b828a..0000000000 --- a/selfdrive/ui/sunnypilot/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 "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 deleted file mode 100644 index 193fdae0dc..0000000000 --- a/selfdrive/ui/sunnypilot/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/selfdrive/ui/sunnypilot/qt/request_repeater.cc b/selfdrive/ui/sunnypilot/qt/request_repeater.cc deleted file mode 100644 index e137d56172..0000000000 --- a/selfdrive/ui/sunnypilot/qt/request_repeater.cc +++ /dev/null @@ -1,41 +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/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 deleted file mode 100644 index 0cdb370f9b..0000000000 --- a/selfdrive/ui/sunnypilot/qt/request_repeater.h +++ /dev/null @@ -1,33 +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/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/qt/sidebar.cc b/selfdrive/ui/sunnypilot/qt/sidebar.cc deleted file mode 100644 index a4dc1d6763..0000000000 --- a/selfdrive/ui/sunnypilot/qt/sidebar.cc +++ /dev/null @@ -1,54 +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/sidebar.h" - -#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); -} - -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::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 deleted file mode 100644 index 133106cfdf..0000000000 --- a/selfdrive/ui/sunnypilot/qt/sidebar.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 "selfdrive/ui/qt/sidebar.h" - -#include "selfdrive/ui/sunnypilot/ui.h" - -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); - -public: - explicit SidebarSP(QWidget *parent = 0); - -private: - 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 deleted file mode 100644 index eaa1f4bd12..0000000000 --- a/selfdrive/ui/sunnypilot/qt/util.cc +++ /dev/null @@ -1,128 +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/util.h" -#include "selfdrive/ui/qt/util.h" - -#include -#include - -#include -#include -#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"); -} - -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; -} - -/** - * @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().replace(QRegularExpression("[^a-zA-Z0-9\\s]"), " ").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(); - QString normalized_element = element.normalized(QString::NormalizationForm_KD).toLower(); - return normalized_element.contains(normalized_term, Qt::CaseInsensitive); - })) { - search_results << element; - } - } - return search_results; -} - -std::optional loadCerealEvent(Params& params, const std::string& _param) { - std::string bytes = params.get(_param); - - try { - AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(bytes.data(), bytes.size())); - return cmsg.getRoot(); - } catch (kj::Exception& e) { - qInfo() << "invalid " << QString::fromStdString(_param) << ":" << e.getDescription().cStr(); - return std::nullopt; - } -} - -bool hasIntelligentCruiseButtonManagement(const cereal::CarParamsSP::Reader &car_params_sp) { - return car_params_sp.getIntelligentCruiseButtonManagementAvailable() && Params().getBool("IntelligentCruiseButtonManagement"); -} diff --git a/selfdrive/ui/sunnypilot/qt/util.h b/selfdrive/ui/sunnypilot/qt/util.h deleted file mode 100644 index 60a73615ba..0000000000 --- a/selfdrive/ui/sunnypilot/qt/util.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 -#include - -#include -#include -#include -#include - -#include "selfdrive/ui/sunnypilot/ui.h" - -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); -std::optional loadCerealEvent(Params& params, const std::string& _param); -bool hasIntelligentCruiseButtonManagement(const cereal::CarParamsSP::Reader &car_params_sp); diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc deleted file mode 100644 index 28374f3a9f..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ /dev/null @@ -1,271 +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/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; -} - -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 -std::vector AbstractControlSP::advanced_controls_; -AbstractControlSP::~AbstractControlSP() { UnregisterAdvancedControl(this); } - -void AbstractControlSP::RegisterAdvancedControl(AbstractControlSP *ctrl) { advanced_controls_.push_back(ctrl); } - -void AbstractControlSP::UnregisterAdvancedControl(AbstractControlSP *ctrl) { - advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), ctrl), advanced_controls_.end()); -} - -void AbstractControlSP::UpdateAllAdvancedControls() { - bool visibility = Params().getBool("ShowAdvancedControls"); - advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), nullptr), advanced_controls_.end()); - for (auto *ctrl : advanced_controls_) ctrl->setVisible(visibility); -} - -AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) - : AbstractControl(title, desc, icon, parent), isAdvancedControl(advancedControl) { - if (isAdvancedControl) RegisterAdvancedControl(this); - - main_layout = new QVBoxLayout(this); - main_layout->setMargin(0); - - 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, bool advancedControl) - : AbstractControlSP(title, desc, icon, parent, advancedControl) { - - 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: 0 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); - } else if (!isVisible && spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { - main_layout->insertItem(main_layout->indexOf(description), spacingItem); - } - } - }); - } else { - main_layout->addSpacing(20); - } - - main_layout->addLayout(hlayout); - if (!desc.isEmpty() && spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { - main_layout->insertItem(main_layout->count(), spacingItem); - } - - // 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); - - main_layout->addStretch(); -} - -void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { - if (description != nullptr) { - description->hide(); - } - - if (spacingItem != nullptr && main_layout->indexOf(spacingItem) == -1) { - main_layout->insertItem(main_layout->indexOf(description), spacingItem); - } -} - -// controls - -ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent, bool advancedControl) - : AbstractControlSP(title, desc, "", parent, advancedControl) { - - btn.setText(text); - btn.setStyleSheet(R"( - 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, bool advancedControl) - : ToggleControlSP(title, desc, icon, false, parent, advancedControl){ - - 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 deleted file mode 100644 index 71041556b7..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ /dev/null @@ -1,772 +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 - -#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); -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()) { - 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: - ~AbstractControlSP(); - 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); - } - - void setVisible(bool visible) override { - bool _visible = visible; - if (isAdvancedControl && !params.getBool("ShowAdvancedControls")) { - _visible = false; - } - AbstractControl::setVisible(_visible); - } - - static void RegisterAdvancedControl(AbstractControlSP *ctrl); - static void UnregisterAdvancedControl(AbstractControlSP *ctrl); - static void UpdateAllAdvancedControls(); - -protected: - AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr, bool advancedControl = false); - void hideEvent(QHideEvent *e) override; - - QVBoxLayout *main_layout; - ElidedLabelSP *value; - QLabel *description = nullptr; - bool isAdvancedControl; - -private: - Params params; - static std::vector advanced_controls_; -}; - -// AbstractControlSP_SELECTOR - -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, bool advancedControl = false); - void hideEvent(QHideEvent *e) override; - -}; - -// 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, bool advancedControl = false); - 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, bool advancedControl = false) : AbstractControlSP(title, desc, icon, parent, advancedControl) { - // 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, bool advancedControl = false); - 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 MultiButtonControlSP : public AbstractControlSP_SELECTOR { - Q_OBJECT - -public: - MultiButtonControlSP(const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), button_texts(button_texts), is_inline_layout(inline_layout) { - const QString style = R"( - QPushButton { - border-radius: 20px; - 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; - } - )"; - - if (inline_layout) { - button_param_layout->setMargin(0); - button_param_layout->setSpacing(0); - spacingItem = nullptr; - if (!title.isEmpty()) { - main_layout->removeWidget(title_label); - hlayout->addWidget(title_label, 1); - } - } - - 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->setStyleSheet(style); - button->setMinimumWidth(minimum_button_width); - if (i == 0) button_param_layout->addSpacing(2); - button_param_layout->addWidget(button); - button_group->addButton(button, i); - } - - 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), this, &MultiButtonControlSP::buttonClicked); - } - - 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 setEnableSelectedButtons(bool enable, const std::vector& enabled_btns = {}) const { - for (int i = 0; i < button_group->buttons().size(); i++) { - // 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); - } - } - -protected: - QButtonGroup *button_group; - void paintEvent(QPaintEvent *event) override { - if (is_inline_layout) { - return; - } - - 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 buttonClicked(int id); - -private: - std::string key; - Params params; - std::vector button_texts; - - bool button_group_enabled = true; - bool is_inline_layout; - QHBoxLayout *button_param_layout = is_inline_layout ? new QHBoxLayout() : hlayout; -}; - -class ButtonParamControlSP : public MultiButtonControlSP { - Q_OBJECT -public: - ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 380, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon, - button_texts, minimum_button_width, inline_layout, advancedControl) { - key = param.toStdString(); - int value = atoi(params.get(key).c_str()); - - if (value > 0 && value < button_group->buttons().size()) { - button_group->button(value)->setChecked(true); - } - - QObject::connect(this, QOverload::of(&MultiButtonControlSP::buttonClicked), [=](int id) { - params.put(key, std::to_string(id)); - }); - } - - void refresh() { - int value = atoi(params.get(key).c_str()); - button_group->button(value)->setChecked(true); - } - - void showEvent(QShowEvent *event) override { - refresh(); - } - -private: - std::string key; - Params params; -}; - -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(1); - } - 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 - -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; - return result.toInt(); - } - - int getParamValueScaled() { - const auto param_value = QString::fromStdString(params.get(key)); - return std::nearbyint(param_value.toFloat() * 100.0f); - } - - void setParamValueScaled(const int new_value) { - const float scaled_value = new_value / 100.0f; - params.put(key, QString::number(scaled_value, 'f', 2).toStdString()); - } - - // Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value. - void setParamValue(const int new_value) { - const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value); - 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, const bool inline_layout = false, - const QMap *valMap = nullptr, bool scale_float = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), _title(title), valueMap(valMap), is_inline_layout(inline_layout), use_float_scaling(scale_float) { - const QString style = R"( - QPushButton { - border-radius: 20px; - 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; - } - )"; - - if (inline_layout) { - optionSelectorLayout->setMargin(0); - optionSelectorLayout->setSpacing(0); - if (!title.isEmpty()) { - main_layout->removeWidget(title_label); - hlayout->addWidget(title_label, 1); - } - main_layout->removeItem(spacingItem); - spacingItem = nullptr; - } - - label.setStyleSheet(label_enabled_style); - label.setFixedWidth(inline_layout ? 350 : 300); - label.setAlignment(Qt::AlignCenter); - - const std::vector button_texts{"-", "+"}; - - key = param.toStdString(); - value = use_float_scaling ? getParamValueScaled() : getParamValue(); - - 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; }")); - optionSelectorLayout->addWidget(button, 0, ((i == 0) ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter); - 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, [=]() { - int change_value = (i == 0) ? -per_value_change : per_value_change; - value = use_float_scaling ? getParamValueScaled() : getParamValue(); - value += change_value; - value = std::clamp(value, range.min_value, range.max_value); - - if (use_float_scaling) { - setParamValueScaled(value); - } else { - setParamValue(value); - } - - button_group->button(0)->setEnabled(!(value <= range.min_value)); - button_group->button(1)->setEnabled(!(value >= range.max_value)); - - updateLabels(); - - if (request_update) { - emit updateOtherToggles(); - } - }); - } - - optionSelectorLayout->setAlignment(Qt::AlignLeft); - if (is_inline_layout) { - QFrame *container = new QFrame; - container->setLayout(optionSelectorLayout); - container->setStyleSheet("background-color: #393939; border-radius: 20px;"); - hlayout->addWidget(container); - } - } - - void setUpdateOtherToggles(bool _update) { - request_update = _update; - } - - void setFixedWidth(int width) { - label.setFixedWidth(width); - } - - 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 { - if (is_inline_layout) { - return; - } - - 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 < optionSelectorLayout->count(); ++i) { - QWidget *widget = qobject_cast(optionSelectorLayout->itemAt(i)->widget()); - if (widget) { - w += widget->width(); - } - } - - // 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); - } - -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 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;"; - - bool button_enabled = true; - bool use_float_scaling = false; -}; - -class PushButtonSP : public QPushButton { - Q_OBJECT - -public: - 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; - font-weight: 450; - height: 150px; - padding: 0 25px 0 25px; - color: #FFFFFF; - } - )"; - - 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(); - } - } - -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_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; - QString pressed_style = enabled ? btn_on_pressed_style : btn_off_pressed_style; - setStyleSheet(buttonStyle + enabled_style + pressed_style + btn_disabled_style); - } -}; - -class PanelBackButton : public QPushButton { - Q_OBJECT - -public: - PanelBackButton(const QString &label = "Back", QWidget *parent = nullptr) : QPushButton(label, parent) { - setObjectName("back_btn"); - setFixedSize(400, 100); - } -}; - -inline QString RainbowizeWords(const QString &text) { - const QStringList colors = { - "#FF6F61", // soft coral red - "#FFA177", // warm peach - "#FFD966", // soft golden yellow - "#88D498", // mint green - "#6EC6FF", // sky blue - "#A78BFA", // soft lavender - "#F78FB3" // rose pink - }; - - QString result; - QStringList words = text.split(' '); - - for (int i = 0; i < words.size(); ++i) { - result += QString("%2 ").arg(colors[i % colors.size()]).arg(words[i].toHtmlEscaped()); - } - - return result.trimmed(); - } diff --git a/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc b/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc deleted file mode 100644 index bfdcac2564..0000000000 --- a/selfdrive/ui/sunnypilot/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 "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, 30), 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 deleted file mode 100644 index 0b2802ed8c..0000000000 --- a/selfdrive/ui/sunnypilot/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/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.cc b/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.cc deleted file mode 100644 index b22684a295..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.cc +++ /dev/null @@ -1,31 +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/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->setVisible(false); - QVBoxLayout *collapsible_layout = new QVBoxLayout(); - collapsibleWidget->setLayout(collapsible_layout); - collapsible_layout->setContentsMargins(0, 0, 0, 0); - - 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 deleted file mode 100644 index 4bfcb9ef55..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h +++ /dev/null @@ -1,50 +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/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; -}; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc deleted file mode 100644 index f951187511..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc +++ /dev/null @@ -1,170 +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/widgets/external_storage.h" - -#include -#include -#include -#include -#include - -#include "common/params.h" -#include "selfdrive/ui/qt/api.h" -#include "selfdrive/ui/qt/widgets/input.h" -#include "selfdrive/ui/sunnypilot/ui.h" - -ExternalStorageControl::ExternalStorageControl() : - ButtonControl(tr("External Storage"), "", tr("Extend your comma device's storage by inserting a USB drive into the aux port.")) { - - QObject::connect(this, &ButtonControl::clicked, [=]() { - if (text() == tr("CHECK") || text() == tr("MOUNT")) { - mountStorage(); - } else if (text() == tr("UNMOUNT")) { - unmountStorage(); - } else if (text() == tr("FORMAT")) { - if (ConfirmationDialog::confirm(tr("Are you sure you want to format this drive? This will erase all data."), tr("Format"), this)) { - formatStorage(); - } - } - }); - - QObject::connect(uiState(), &UIState::offroadTransition, this, &ExternalStorageControl::updateState); - updateState(!uiState()->scene.started); - - refresh(); -} - -void ExternalStorageControl::updateState(bool offroad) { - setEnabled(offroad); -} - -void ExternalStorageControl::debouncedRefresh() { - if (refreshPending) return; - refreshPending = true; - - QTimer::singleShot(250, this, [=]() { - refreshPending = false; - refresh(); - }); -} - -void ExternalStorageControl::refresh() { - QtConcurrent::run([=]() { - auto run = [](const QString &cmd) { - QProcess p; - p.start("sh", QStringList() << "-c" << cmd); - p.waitForFinished(); - return p.exitCode() == 0; - }; - - bool isMounted = run("findmnt -n /mnt/external_realdata"); - bool hasDrive = run("lsblk -f /dev/sdg"); - bool hasFs = run("lsblk -f /dev/sdg1 | grep -q ext4"); - bool hasLabel = run("sudo blkid /dev/sdg1 | grep -q 'LABEL=\"openpilot\"'"); - - QString info; - if (isMounted && hasLabel) { - QProcess df; - df.start("sh", QStringList() << "-c" << "df -h /mnt/external_realdata | awk 'NR==2 {print $3 \"/\" $2}'"); - df.waitForFinished(); - info = df.readAllStandardOutput().trimmed(); - } - - QMetaObject::invokeMethod(this, [=]() { - if (formatting) { - setValue(tr("formatting")); - setText(tr("FORMAT")); - setEnabled(false); - } else { - if (!hasDrive) { - setValue(tr("insert drive")); - setText(tr("CHECK")); - } else if (!hasFs || !hasLabel) { - setValue(tr("needs format")); - setText(tr("FORMAT")); - } else if (isMounted) { - setValue(info); - setText(tr("UNMOUNT")); - } else { - setValue("drive detected"); - setText(tr("MOUNT")); - } - updateState(!uiState()->scene.started); - } - }, Qt::QueuedConnection); - }); -} - -void ExternalStorageControl::mountStorage() { - setValue(tr("mounting")); - setEnabled(false); - - QtConcurrent::run([=]() { - QProcess process; - process.start("sh", QStringList() << "-c" << - "sudo mount -o remount,rw / && " - "sudo mkdir -p /mnt/external_realdata && " - "grep -q '/dev/sdg1 /mnt/external_realdata' /etc/fstab || " - "echo '/dev/sdg1 /mnt/external_realdata ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab && " - "sudo systemctl daemon-reexec && " - "sudo mount /mnt/external_realdata && " - "sudo chown -R comma:comma /mnt/external_realdata && " - "sudo chmod -R 775 /mnt/external_realdata && " - "sudo mount -o remount,ro /"); - process.waitForFinished(); - - QMetaObject::invokeMethod(this, [=]() { - debouncedRefresh(); - }, Qt::QueuedConnection); - }); -} - -void ExternalStorageControl::unmountStorage() { - setValue(tr("unmounting")); - setEnabled(false); - - QtConcurrent::run([=]() { - QProcess process; - process.start("sh", QStringList() << "-c" << "sudo umount /mnt/external_realdata"); - process.waitForFinished(); - - QMetaObject::invokeMethod(this, [=]() { - debouncedRefresh(); - }, Qt::QueuedConnection); - }); -} - -void ExternalStorageControl::formatStorage() { - unmountStorage(); - formatting = true; - setValue(tr("formatting")); - setEnabled(false); - - QProcess *process = new QProcess(this); - connect(process, static_cast(&QProcess::finished), - this, [=](int exitCode, QProcess::ExitStatus status) { - process->deleteLater(); - formatting = false; - if (exitCode == 0 && status == QProcess::NormalExit) { - mountStorage(); - } else { - setValue(tr("needs format")); - updateState(!uiState()->scene.started); - } - }); - process->start("sh", QStringList() << "-c" << - "sudo wipefs -a /dev/sdg && " - "sudo parted -s /dev/sdg mklabel gpt mkpart primary ext4 0% 100% && " - "sudo mkfs.ext4 -F -L openpilot /dev/sdg1" - ); -} - -void ExternalStorageControl::showEvent(QShowEvent *event) { - ButtonControl::showEvent(event); - QTimer::singleShot(100, this, &ExternalStorageControl::debouncedRefresh); -} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h deleted file mode 100644 index d26eefd18c..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h +++ /dev/null @@ -1,34 +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 "system/hardware/hw.h" -#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" -#define ButtonControl ButtonControlSP - -class ExternalStorageControl : public ButtonControl { - Q_OBJECT - -public: - ExternalStorageControl(); - -protected: - void showEvent(QShowEvent *event) override; - -private: - Params params; - - bool refreshPending = false; - bool formatting = false; - void updateState(bool offroad); - void refresh(); - void debouncedRefresh(); - void mountStorage(); - void unmountStorage(); - void formatStorage(); -}; diff --git a/selfdrive/ui/sunnypilot/qt/widgets/prime.cc b/selfdrive/ui/sunnypilot/qt/widgets/prime.cc deleted file mode 100644 index a1bb85c787..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/prime.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 "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 deleted file mode 100644 index 598b0e62eb..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/prime.h +++ /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. - */ - -#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/sunnypilot/qt/widgets/scrollview.cc b/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc deleted file mode 100644 index ba9a367b0e..0000000000 --- a/selfdrive/ui/sunnypilot/qt/widgets/scrollview.cc +++ /dev/null @@ -1,18 +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/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 deleted file mode 100644 index 5c20bce2f2..0000000000 --- a/selfdrive/ui/sunnypilot/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/selfdrive/ui/sunnypilot/qt/widgets/toggle.cc b/selfdrive/ui/sunnypilot/qt/widgets/toggle.cc deleted file mode 100644 index 601c8f94c1..0000000000 --- a/selfdrive/ui/sunnypilot/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 "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 deleted file mode 100644 index e98dea39af..0000000000 --- a/selfdrive/ui/sunnypilot/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/selfdrive/ui/sunnypilot/qt/window.cc b/selfdrive/ui/sunnypilot/qt/window.cc deleted file mode 100644 index f332e1ed8e..0000000000 --- a/selfdrive/ui/sunnypilot/qt/window.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 "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 deleted file mode 100644 index d4c0900901..0000000000 --- a/selfdrive/ui/sunnypilot/qt/window.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/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 deleted file mode 100644 index df7b3ce5ca..0000000000 --- a/selfdrive/ui/sunnypilot/ui.cc +++ /dev/null @@ -1,122 +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/ui.h" - -#include "common/watchdog.h" - -void UIStateSP::updateStatus() { - UIState::updateStatus(); - - if (scene.started && scene.onroadScreenOffControl) { - auto selfdriveState = (*sm)["selfdriveState"].getSelfdriveState(); - if (selfdriveState.getAlertSize() != cereal::SelfdriveState::AlertSize::NONE) { - reset_onroad_sleep_timer(); - } else if (scene.onroadScreenOffTimer > 0) { - scene.onroadScreenOffTimer--; - } - } -} - -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", - "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", - "carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters", - "carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP" - }); - - // update timer - timer = new QTimer(this); - QObject::connect(timer, &QTimer::timeout, this, &UIStateSP::update); - timer->start(1000 / UI_FREQ); - - // Param watcher for UIScene param updates - param_watcher = new ParamWatcher(this); - connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { - ui_update_params_sp(this); - }); - param_watcher->addParam("DevUIInfo"); - param_watcher->addParam("StandstillTimer"); -} - -// 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); -} - -void ui_update_params_sp(UIStateSP *s) { - auto params = Params(); - s->scene.dev_ui_info = std::atoi(params.get("DevUIInfo").c_str()); - s->scene.standstill_timer = params.getBool("StandstillTimer"); - s->scene.speed_limit_mode = std::atoi(params.get("SpeedLimitMode").c_str()); - s->scene.road_name = params.getBool("RoadNameToggle"); - s->scene.trueVEgoUI = params.getBool("TrueVEgoUI"); - s->scene.hideVEgoUI = params.getBool("HideVEgoUI"); - - // Onroad Screen Brightness - s->scene.onroadScreenOffBrightness = std::atoi(params.get("OnroadScreenOffBrightness").c_str()); - s->scene.onroadScreenOffControl = params.getBool("OnroadScreenOffControl"); - s->scene.onroadScreenOffTimerParam = std::atoi(params.get("OnroadScreenOffTimer").c_str()); - - s->scene.turn_signals = params.getBool("ShowTurnSignals"); - s->scene.chevron_info = std::atoi(params.get("ChevronInfo").c_str()); - s->scene.blindspot_ui = params.getBool("BlindSpot"); - s->scene.rainbow_mode = params.getBool("RainbowMode"); -} - -void UIStateSP::reset_onroad_sleep_timer(OnroadTimerStatusToggle toggleTimerStatus) { - // Toggling from active state to inactive - if (toggleTimerStatus == OnroadTimerStatusToggle::PAUSE and scene.onroadScreenOffTimer != -1) { - scene.onroadScreenOffTimer = -1; - } - // Toggling from a previously inactive state or resetting an active timer - else if ((scene.onroadScreenOffTimerParam >= 0 and scene.onroadScreenOffControl and scene.onroadScreenOffTimer != -1) or toggleTimerStatus == OnroadTimerStatusToggle::RESUME) { - scene.onroadScreenOffTimer = scene.onroadScreenOffTimerParam * UI_FREQ; - } -} - -DeviceSP::DeviceSP(QObject *parent) : Device(parent) { - QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); - QObject::connect(this, &Device::displayPowerChanged, this, &DeviceSP::handleDisplayPowerChanged); -} - -UIStateSP *uiStateSP() { - static UIStateSP ui_state; - 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; -} - -void DeviceSP::handleDisplayPowerChanged(bool on) { - // if enabled, trigger offroad mode when device goes to sleep - if (params.get("DeviceBootMode") == "1" && not on) { - params.putBool("OffroadMode", true); - } -} diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h deleted file mode 100644 index ba5de83ef6..0000000000 --- a/selfdrive/ui/sunnypilot/ui.h +++ /dev/null @@ -1,105 +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/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" -#include "selfdrive/ui/qt/util.h" - -enum OnroadTimerStatusToggle { - NONE, - PAUSE, - RESUME -}; - -class UIStateSP : public UIState { - Q_OBJECT - -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); - - 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"; - }); - } - void reset_onroad_sleep_timer(OnroadTimerStatusToggle toggleTimerStatus = OnroadTimerStatusToggle::NONE); - -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 = {}; - ParamWatcher *param_watcher; -}; - -UIStateSP *uiStateSP(); -inline UIStateSP *uiState() { return uiStateSP(); }; - -// device management class -class DeviceSP : public Device { - Q_OBJECT - -public: - DeviceSP(QObject *parent = 0); - -private: - Params params; - void handleDisplayPowerChanged(bool on); -}; - -DeviceSP *deviceSP(); -inline DeviceSP *device() { return deviceSP(); } - -void ui_update_params_sp(UIStateSP *s); diff --git a/selfdrive/ui/sunnypilot/ui_scene.h b/selfdrive/ui/sunnypilot/ui_scene.h deleted file mode 100644 index da98931e6b..0000000000 --- a/selfdrive/ui/sunnypilot/ui_scene.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 - -typedef struct UISceneSP : UIScene { - int dev_ui_info = 0; - bool standstill_timer = false; - int speed_limit_mode = 0; - bool road_name = false; - int onroadScreenOffBrightness, onroadScreenOffTimer = 0; - bool onroadScreenOffControl; - int onroadScreenOffTimerParam; - bool trueVEgoUI; - bool hideVEgoUI; - bool turn_signals = false; - int chevron_info; - bool blindspot_ui; - bool rainbow_mode; -} UISceneSP; diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index 91898ac59a..d926a7ae86 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -1,3 +1,4 @@ test test_translations -test_ui/report_1 \ No newline at end of file +test_ui/report_1 +test_ui/raylib_report diff --git a/selfdrive/ui/tests/create_test_translations.sh b/selfdrive/ui/tests/create_test_translations.sh deleted file mode 100755 index ed0890d946..0000000000 --- a/selfdrive/ui/tests/create_test_translations.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -e - -UI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"/.. -TEST_TEXT="(WRAPPED_SOURCE_TEXT)" -TEST_TS_FILE=$UI_DIR/translations/main_test_en.ts -TEST_QM_FILE=$UI_DIR/translations/main_test_en.qm - -# translation strings -UNFINISHED="<\/translation>" -TRANSLATED="$TEST_TEXT<\/translation>" - -mkdir -p $UI_DIR/translations -rm -f $TEST_TS_FILE $TEST_QM_FILE -lupdate -recursive "$UI_DIR" -ts $TEST_TS_FILE -sed -i "s/$UNFINISHED/$TRANSLATED/" $TEST_TS_FILE -lrelease $TEST_TS_FILE diff --git a/selfdrive/ui/tests/cycle_offroad_alerts.py b/selfdrive/ui/tests/cycle_offroad_alerts.py index 04fcd4017f..b577b74b00 100755 --- a/selfdrive/ui/tests/cycle_offroad_alerts.py +++ b/selfdrive/ui/tests/cycle_offroad_alerts.py @@ -7,6 +7,7 @@ import json from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert +from openpilot.system.updated.updated import parse_release_notes if __name__ == "__main__": params = Params() @@ -18,9 +19,7 @@ if __name__ == "__main__": while True: print("setting alert update") params.put_bool("UpdateAvailable", True) - r = open(os.path.join(BASEDIR, "CHANGELOG.md")).read() - r = r[:r.find('\n\n')] # Slice latest release notes - params.put("UpdaterNewReleaseNotes", r + "\n") + params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR)) time.sleep(t) params.put_bool("UpdateAvailable", False) diff --git a/selfdrive/ui/tests/profile_onroad.py b/selfdrive/ui/tests/profile_onroad.py new file mode 100755 index 0000000000..0294125ceb --- /dev/null +++ b/selfdrive/ui/tests/profile_onroad.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +import os +import time +import cProfile +import pyray as rl +import numpy as np + +from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.layouts.main import MainLayout +from openpilot.system.ui.lib.application import gui_app +from openpilot.tools.lib.logreader import LogReader +from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE + +FPS = 60 + + +def chunk_messages_by_time(messages): + dt_ns = 1e9 / FPS + chunks = [] + current_services = {} + next_time = messages[0].logMonoTime + dt_ns if messages else 0 + + for msg in messages: + if msg.logMonoTime >= next_time: + chunks.append(current_services) + current_services = {} + next_time += dt_ns * ((msg.logMonoTime - next_time) // dt_ns + 1) + current_services[msg.which()] = msg + + if current_services: + chunks.append(current_services) + return chunks + + +def patch_submaster(message_chunks): + def mock_update(timeout=None): + sm = ui_state.sm + sm.updated = dict.fromkeys(sm.services, False) + current_time = time.monotonic() + for service, msg in message_chunks[sm.frame].items(): + if service in sm.data: + sm.seen[service] = True + sm.updated[service] = True + + msg_builder = msg.as_builder() + sm.data[service] = getattr(msg_builder, service) + sm.logMonoTime[service] = msg.logMonoTime + sm.recv_time[service] = current_time + sm.recv_frame[service] = sm.frame + sm.valid[service] = True + sm.frame += 1 + ui_state.sm.update = mock_update + + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description='Profile openpilot UI rendering and state updates') + parser.add_argument('route', type=str, nargs='?', default=DEMO_ROUTE + "/1", + help='Route to use for profiling') + parser.add_argument('--loop', type=int, default=1, + help='Number of times to loop the log (default: 1)') + parser.add_argument('--output', type=str, default='cachegrind.out.ui', + help='Output file prefix (default: cachegrind.out.ui)') + parser.add_argument('--max-seconds', type=float, default=None, + help='Maximum seconds of messages to process (default: all)') + parser.add_argument('--headless', action='store_true', + help='Run in headless mode without GPU (for CI/testing)') + args = parser.parse_args() + + print(f"Loading log from {args.route}...") + lr = LogReader(args.route, sort_by_time=True) + messages = list(lr) * args.loop + + print("Chunking messages...") + message_chunks = chunk_messages_by_time(messages) + if args.max_seconds: + message_chunks = message_chunks[:int(args.max_seconds * FPS)] + + print("Initializing UI with GPU rendering...") + + if args.headless: + os.environ['SDL_VIDEODRIVER'] = 'dummy' + + gui_app.init_window("UI Profiling") + main_layout = MainLayout() + main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + + print("Running...") + patch_submaster(message_chunks) + + W, H = 1928, 1208 + vipc = VisionIpcServer("camerad") + vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, 1928, 1208) + vipc.start_listener() + yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2 + yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() + with cProfile.Profile() as pr: + for should_render in gui_app.render(): + if ui_state.sm.frame >= len(message_chunks): + break + if ui_state.sm.frame % 3 == 0: + eof = int((ui_state.sm.frame % 3) * 0.05 * 1e9) + vipc.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data, ui_state.sm.frame % 3, eof, eof) + ui_state.update() + if should_render: + main_layout.render() + pr.dump_stats(f'{args.output}_deterministic.stats') + + rl.close_window() + print("\nProfiling complete!") + print(f" run: python -m pstats {args.output}_deterministic.stats") diff --git a/selfdrive/ui/tests/test_raylib_ui.py b/selfdrive/ui/tests/test_raylib_ui.py index 3f23301972..69ba946dcd 100644 --- a/selfdrive/ui/tests/test_raylib_ui.py +++ b/selfdrive/ui/tests/test_raylib_ui.py @@ -2,7 +2,7 @@ import time from openpilot.selfdrive.test.helpers import with_processes -@with_processes(["raylib_ui"]) +@with_processes(["ui"]) def test_raylib_ui(): """Test initialization of the UI widgets is successful.""" time.sleep(1) diff --git a/selfdrive/ui/tests/test_runner.cc b/selfdrive/ui/tests/test_runner.cc deleted file mode 100644 index c8cc0d3e05..0000000000 --- a/selfdrive/ui/tests/test_runner.cc +++ /dev/null @@ -1,26 +0,0 @@ -#define CATCH_CONFIG_RUNNER -#include "catch2/catch.hpp" - -#include -#include -#include -#include - -int main(int argc, char **argv) { - // unit tests for Qt - QApplication app(argc, argv); - - QString language_file = "main_test_en"; - // FIXME: pytest-cpp considers this print as a test case - qDebug() << "Loading language:" << language_file; - - QTranslator translator; - QString translationsPath = QDir::cleanPath(qApp->applicationDirPath() + "/../translations"); - if (!translator.load(language_file, translationsPath)) { - qDebug() << "Failed to load translation file!"; - } - app.installTranslator(&translator); - - const int res = Catch::Session().run(argc, argv); - return (res < 0xff ? res : 0xff); -} diff --git a/selfdrive/ui/tests/test_translations.cc b/selfdrive/ui/tests/test_translations.cc deleted file mode 100644 index fcefc5784f..0000000000 --- a/selfdrive/ui/tests/test_translations.cc +++ /dev/null @@ -1,48 +0,0 @@ -#include "catch2/catch.hpp" - -#include "common/params.h" -#include "selfdrive/ui/qt/window.h" - -const QString TEST_TEXT = "(WRAPPED_SOURCE_TEXT)"; // what each string should be translated to -QRegExp RE_NUM("\\d*"); - -QStringList getParentWidgets(QWidget* widget){ - QStringList parentWidgets; - while (widget->parentWidget() != Q_NULLPTR) { - widget = widget->parentWidget(); - parentWidgets.append(widget->metaObject()->className()); - } - return parentWidgets; -} - -template -void checkWidgetTrWrap(MainWindow &w) { - for (auto widget : w.findChildren()) { - const QString text = widget->text(); - bool isNumber = RE_NUM.exactMatch(text); - bool wrapped = text.contains(TEST_TEXT); - QString parentWidgets = getParentWidgets(widget).join("->"); - - if (!text.isEmpty() && !isNumber && !wrapped) { - FAIL(("\"" + text + "\" must be wrapped. Parent widgets: " + parentWidgets).toStdString()); - } - - // warn if source string wrapped, but UI adds text - // TODO: add way to ignore this - if (wrapped && text != TEST_TEXT) { - WARN(("\"" + text + "\" is dynamic and needs a custom retranslate function. Parent widgets: " + parentWidgets).toStdString()); - } - } -} - -// Tests all strings in the UI are wrapped with tr() -TEST_CASE("UI: test all strings wrapped") { - Params().remove("LanguageSetting"); - Params().remove("HardwareSerial"); - Params().remove("DongleId"); - qputenv("TICI", "1"); - - MainWindow w; - checkWidgetTrWrap(w); - checkWidgetTrWrap(w); -} diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index 2ae3356bb8..3177814f9f 100644 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -6,10 +6,9 @@ import xml.etree.ElementTree as ET import string import requests from parameterized import parameterized_class +from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, LANGUAGES_FILE -from openpilot.selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE - -with open(LANGUAGES_FILE) as f: +with open(str(LANGUAGES_FILE)) as f: translation_files = json.load(f) UNFINISHED_TRANSLATION_TAG = ", YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:51-0700\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "" + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "" + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "" + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "" + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "" + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "" + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

Steering lag calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "" + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "" + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "" diff --git a/selfdrive/ui/translations/app_ar.po b/selfdrive/ui/translations/app_ar.po new file mode 100644 index 0000000000..608389fc07 --- /dev/null +++ b/selfdrive/ui/translations/app_ar.po @@ -0,0 +1,1218 @@ +# Arabic translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-22 16:32-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0?0:n==1?1:n==2?2:(n%100>=3 && " +"n%100<=10)?3:(n%100>=11 && n%100<=99)?4:5;\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " اكتملت معايرة استجابة عزم التوجيه." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " اكتملت معايرة استجابة عزم التوجيه بنسبة {}٪." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " جهازك موجه بمقدار {:.1f}° {} و {:.1f}° {}." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "سنة واحدة من تخزين القيادة" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "اتصال LTE على مدار الساعة" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"تحذير: التحكم الطولي لـ openpilot في مرحلة ألفا لهذه السيارة وسيُعطّل نظام " +"الكبح التلقائي في حالات الطوارئ (AEB).

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

Steering lag calibration is complete." +msgstr "

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

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

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

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"يعمل openpilot افتراضياً في وضع الهدوء. يفعّل وضع التجربة ميزات بمستوى ألفا " +"غير الجاهزة لوضع الهدوء. الميزات التجريبية مدرجة أدناه:

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


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

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


سينتقل عرض القيادة إلى الكاميرا الواسعة المواجهة للطريق عند " +"السرعات المنخفضة لإظهار بعض المنعطفات بشكل أفضل. كما سيظهر شعار وضع التجربة " +"في الزاوية العلوية اليمنى." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +"يقوم openpilot بالمعايرة بشكل مستمر، ونادراً ما تتطلب إعادة الضبط. ستؤدي " +"إعادة ضبط المعايرة إلى إعادة تشغيل openpilot إذا كانت السيارة قيد التشغيل." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"يتعلم openpilot القيادة بمشاهدة البشر، مثلك، يقودون.\n" +"\n" +"يتيح وضع Firehose زيادة تحميل بيانات التدريب لتحسين نماذج قيادة openpilot. " +"المزيد من البيانات يعني نماذج أكبر، مما يعني وضع تجربة أفضل." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "قد يأتي التحكم الطولي لـ openpilot في تحديث مستقبلي." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"يتطلب openpilot تركيب الجهاز ضمن 4° يساراً أو يميناً وضمن 5° للأعلى أو 9° " +"للأسفل." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "يمين" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "غير محدود" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "أعلى" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "محدّث، آخر تحقق: أبداً" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "محدّث، آخر تحقق {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "تحديث متاح" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} تنبيه" +msgstr[1] "{} تنبيه" +msgstr[2] "{} تنبيهان" +msgstr[3] "{} تنبيهات" +msgstr[4] "{} تنبيهات" +msgstr[5] "{} تنبيه" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "قبل {} يوم" +msgstr[1] "قبل {} يوم" +msgstr[2] "قبل {} يومين" +msgstr[3] "قبل {} أيام" +msgstr[4] "قبل {} أيام" +msgstr[5] "قبل {} يوم" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "قبل {} ساعة" +msgstr[1] "قبل {} ساعة" +msgstr[2] "قبل {} ساعتين" +msgstr[3] "قبل {} ساعات" +msgstr[4] "قبل {} ساعات" +msgstr[5] "قبل {} ساعة" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "قبل {} دقيقة" +msgstr[1] "قبل {} دقيقة" +msgstr[2] "قبل {} دقيقتين" +msgstr[3] "قبل {} دقائق" +msgstr[4] "قبل {} دقائق" +msgstr[5] "قبل {} دقيقة" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "{} مقطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." +msgstr[1] "{} مقطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." +msgstr[2] "{} مقطعان من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." +msgstr[3] "{} مقاطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." +msgstr[4] "{} مقاطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." +msgstr[5] "{} مقطع من قيادتك ضمن مجموعة بيانات التدريب حتى الآن." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ مشترك" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 وضع Firehose 🔥" diff --git a/selfdrive/ui/translations/app_de.po b/selfdrive/ui/translations/app_de.po new file mode 100644 index 0000000000..f32c27a9ef --- /dev/null +++ b/selfdrive/ui/translations/app_de.po @@ -0,0 +1,1221 @@ +# German translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-20 16:35-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " Die Lenkmoment-Reaktionskalibrierung ist abgeschlossen." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " Die Lenkmoment-Reaktionskalibrierung ist zu {}% abgeschlossen." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " Ihr Gerät ist um {:.1f}° {} und {:.1f}° {} ausgerichtet." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 Jahr Fahrtdatenspeicherung" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "24/7 LTE‑Verbindung" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"WARNUNG: Die Längsregelung von openpilot befindet sich für dieses " +"Fahrzeug in der Alpha-Phase und deaktiviert das automatische Notbremssystem " +"(AEB).

Auf diesem Fahrzeug verwendet openpilot standardmäßig den " +"integrierten ACC statt der openpilot-Längsregelung. Aktivieren Sie dies, um " +"auf die openpilot-Längsregelung umzuschalten. Das Aktivieren des " +"Experimentalmodus wird empfohlen, wenn Sie die openpilot-Längsregelung " +"(Alpha) aktivieren." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "

Kalibrierung der Lenkverzögerung abgeschlossen." + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

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

Kalibrierung der Lenkverzögerung zu {}% abgeschlossen." + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "AKTIV" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB (Android Debug Bridge) ermöglicht die Verbindung mit Ihrem Gerät über " +"USB oder über das Netzwerk. Siehe https://docs.comma.ai/how-to/connect-to-" +"comma für weitere Informationen." + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "HINZUFÜGEN" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "APN‑Einstellung" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "Übermäßige Betätigung bestätigen" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "Erweitert" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "Aggressiv" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "Zustimmen" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "Immer aktive Fahrerüberwachung" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"Eine Alpha-Version der openpilot-Längsregelung kann zusammen mit dem " +"Experimentalmodus auf Nicht-Release-Zweigen getestet werden." + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "Sind Sie sicher, dass Sie ausschalten möchten?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "Sind Sie sicher, dass Sie neu starten möchten?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "Sind Sie sicher, dass Sie die Kalibrierung zurücksetzen möchten?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "Sind Sie sicher, dass Sie deinstallieren möchten?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "Zurück" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "Werden Sie comma prime Mitglied auf connect.comma.ai" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "" +"Fügen Sie connect.comma.ai Ihrem Startbildschirm hinzu, um es wie eine App " +"zu verwenden" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "ÄNDERN" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "PRÜFEN" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "CHILL‑MODUS AKTIV" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "VERBINDUNG" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "VERBINDUNG" + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "Abbrechen" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "Getaktete Mobilfunkverbindung" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "Sprache ändern" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "" +" Durch Ändern dieser Einstellung wird openpilot neu gestartet, wenn das Auto " +"eingeschaltet ist." + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "Klicken Sie auf \"add new device\" und scannen Sie den QR‑Code rechts" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "Schließen" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "Aktuelle Version" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "HERUNTERLADEN" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "Ablehnen" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "Ablehnen, openpilot deinstallieren" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "Entwickler" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "Gerät" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "Beim Gaspedal deaktivieren" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "Zum Ausschalten deaktivieren" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "Zum Neustart deaktivieren" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "Zum Zurücksetzen der Kalibrierung deaktivieren" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "Geschwindigkeit in km/h statt mph anzeigen." + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "Dongle-ID" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "Herunterladen" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "Fahrerkamera" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "Fahrstil" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "BEARBEITEN" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "FEHLER" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "EXPERIMENTALMODUS AKTIV" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "Aktivieren" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "ADB aktivieren" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "Spurverlassenswarnungen aktivieren" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "openpilot aktivieren" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "SSH aktivieren" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "Spurverlassenswarnungen aktivieren" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "Fahrerüberwachung auch aktivieren, wenn openpilot nicht aktiv ist." + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "openpilot aktivieren" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"Den Schalter für die openpilot-Längsregelung (Alpha) aktivieren, um den " +"Experimentalmodus zu erlauben." + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "APN eingeben" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "SSID eingeben" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "Neues Tethering‑Passwort eingeben" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "Passwort eingeben" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "Geben Sie Ihren GitHub‑Benutzernamen ein" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "Fehler" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "Experimentalmodus" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"Der Experimentalmodus ist derzeit auf diesem Fahrzeug nicht verfügbar, da " +"der serienmäßige ACC für die Längsregelung verwendet wird." + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "WIRD VERGESSEN..." + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "Einrichtung abschließen" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Firehose‑Modus" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"Für maximale Wirksamkeit bringen Sie Ihr Gerät regelmäßig ins Haus und " +"verbinden es wöchentlich mit einem guten USB‑C‑Adapter und WLAN.\n" +"\n" +"Der Firehose‑Modus kann auch während der Fahrt funktionieren, wenn eine " +"Verbindung zu einem Hotspot oder einer unbegrenzten SIM besteht.\n" +"\n" +"\n" +"Häufig gestellte Fragen\n" +"\n" +"Spielt es eine Rolle, wie oder wo ich fahre? Nein, fahren Sie einfach wie " +"gewöhnlich.\n" +"\n" +"Werden alle meine Segmente im Firehose‑Modus abgeholt? Nein, wir ziehen " +"selektiv eine Teilmenge Ihrer Segmente.\n" +"\n" +"Was ist ein guter USB‑C‑Adapter? Jeder schnelle Telefon‑ oder Laptoplader " +"sollte ausreichen.\n" +"\n" +"Spielt es eine Rolle, welche Software ich verwende? Ja, nur " +"Upstream‑openpilot (und bestimmte Forks) können für das Training verwendet " +"werden." + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "Vergessen" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "WLAN‑Netz „{}“ vergessen?" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "GUT" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "Gehen Sie auf Ihrem Telefon zu https://connect.comma.ai" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "HOCH" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "Netzwerk" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "INAKTIV: Mit einem unlimitierten Netzwerk verbinden" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "INSTALLIEREN" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "IP‑Adresse" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "Update installieren" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "Joystick‑Debugmodus" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "LADEN" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "Längsmanövermodus" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "MAX" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" +"Maximieren Sie Ihre Trainingsdaten‑Uploads, um die Fahrmodelle von openpilot " +"zu verbessern." + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "k. A." + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "KEIN" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "Netzwerk" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "Keine SSH‑Schlüssel gefunden" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "Keine SSH‑Schlüssel für Benutzer '{username}' gefunden" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "Keine Versionshinweise verfügbar." + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "OFFLINE" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "OK" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "ONLINE" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "Öffnen" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "KOPPELN" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "VORSCHAU" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "PRIME‑FUNKTIONEN:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "Gerät koppeln" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "Gerät koppeln" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "Koppeln Sie Ihr Gerät mit Ihrem comma‑Konto" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"Koppeln Sie Ihr Gerät mit comma connect (connect.comma.ai) und lösen Sie Ihr " +"comma‑prime‑Angebot ein." + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "Bitte mit WLAN verbinden, um das erste Koppeln abzuschließen" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "Ausschalten" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"Vorschau der Fahrer‑Kamera, um sicherzustellen, dass die Fahrerüberwachung " +"gute Sicht hat. (Fahrzeug muss ausgeschaltet sein)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "QR‑Code‑Fehler" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "ENTFERNEN" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "ZURÜCKSETZEN" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "ANSEHEN" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "Neustart" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "Gerät neu starten" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "Neustarten und aktualisieren" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"Erhalten Sie Warnungen, um zurück in die Spur zu lenken, wenn Ihr Fahrzeug " +"ohne Blinker über eine erkannte Spurlinie driftet und über 31 mph (50 km/h) " +"fährt." + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "Fahrerkamera aufzeichnen und hochladen" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "Mikrofonton aufzeichnen und hochladen" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"Mikrofonton während der Fahrt aufzeichnen und speichern. Die Audiospur wird " +"im Dashcam‑Video in comma connect enthalten sein." + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "Vorschriften" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "Entspannt" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "Fernzugriff" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "Remote‑Schnappschüsse" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "Zeitüberschreitung bei der Anfrage" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "Zurücksetzen" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "Kalibrierung zurücksetzen" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "Trainingsanleitung ansehen" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "" +"Überprüfen Sie die Regeln, Funktionen und Einschränkungen von openpilot" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "SSH‑Schlüssel" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "WLAN‑Netzwerke werden gesucht..." + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "Auswählen" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "Sprache auswählen" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "Seriennummer" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "Update verschieben" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "Software" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "Standard" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"Standard wird empfohlen. Im aggressiven Modus folgt openpilot " +"vorausfahrenden Fahrzeugen näher und ist beim Gasgeben und Bremsen " +"aggressiver. Im entspannten Modus bleibt openpilot weiter entfernt. Bei " +"unterstützten Fahrzeugen können Sie mit der Abstandstaste am Lenkrad " +"zwischen diesen Profilen wechseln." + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "System reagiert nicht" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "SOFORT DIE KONTROLLE ÜBERNEHMEN" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "TEMP" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "Tethering‑Passwort" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "Schalter" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "DEINSTALLIEREN" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "UPDATE" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "Deinstallieren" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "Unbekannt" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "Updates werden nur heruntergeladen, wenn das Auto aus ist." + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "Jetzt abonnieren" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"Daten von der Fahrer‑Kamera hochladen und den Fahrerüberwachungs‑Algorithmus " +"verbessern." + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "Metersystem verwenden" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"Verwenden Sie openpilot für adaptive Geschwindigkeitsregelung und " +"Spurhalteassistenz. Ihre Aufmerksamkeit ist jederzeit erforderlich, um diese " +"Funktion zu nutzen." + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "FAHRZEUG" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "ANSEHEN" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "Warten auf Start" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"Warnung: Dies gewährt SSH‑Zugriff auf alle öffentlichen Schlüssel in Ihren " +"GitHub‑Einstellungen. Geben Sie niemals einen anderen GitHub‑Benutzernamen " +"als Ihren eigenen ein. Ein comma‑Mitarbeiter wird Sie NIEMALS bitten, seinen " +"GitHub‑Benutzernamen hinzuzufügen." + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "Willkommen bei openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "Wenn aktiviert, deaktiviert das Drücken des Gaspedals openpilot." + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "WLAN" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "Getaktetes WLAN‑Netzwerk" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "Falsches Passwort" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "" +"Sie müssen die Nutzungsbedingungen akzeptieren, um openpilot zu verwenden." + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"Sie müssen die Nutzungsbedingungen akzeptieren, um openpilot zu verwenden. " +"Lesen Sie die aktuellen Bedingungen unter https://comma.ai/terms, bevor Sie " +"fortfahren." + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "Kamera startet" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "Standard" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "unten" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "Überprüfung auf Updates fehlgeschlagen" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "für „{}“" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "km/h" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "für automatische Konfiguration leer lassen" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "links" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "getaktet" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "mph" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "nie" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "jetzt" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "openpilot Längsregelung (Alpha)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot nicht verfügbar" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot fährt standardmäßig im Chill‑Modus. Der Experimentalmodus " +"aktiviert Funktionen im Alpha‑Status, die für den Chill‑Modus noch nicht " +"bereit sind. Die experimentellen Funktionen sind unten aufgeführt:" +"

End-to‑End‑Längsregelung


Das Fahrmodell steuert Gas und " +"Bremse. openpilot fährt so, wie es einen Menschen einschätzt, einschließlich " +"Anhalten an roten Ampeln und Stoppschildern. Da das Modell die " +"Geschwindigkeit bestimmt, dient die eingestellte Geschwindigkeit nur als " +"Obergrenze. Dies ist eine Alpha‑Funktion; Fehler sind zu erwarten." +"

Neue Fahrvisualisierung


Die Visualisierung wechselt bei " +"niedriger Geschwindigkeit auf die nach vorn gerichtete Weitwinkelkamera, um " +"manche Kurven besser zu zeigen. Das Experimentalmodus‑Logo wird außerdem " +"oben rechts angezeigt." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +" Durch Ändern dieser Einstellung wird openpilot neu gestartet, wenn das Auto " +"eingeschaltet ist." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot lernt das Fahren, indem es Menschen wie Sie beobachtet.\n" +"\n" +"Der Firehose‑Modus ermöglicht es Ihnen, Ihre Trainingsdaten‑Uploads zu " +"maximieren, um die Fahrmodelle von openpilot zu verbessern. Mehr Daten " +"bedeuten größere Modelle – und damit einen besseren Experimentalmodus." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "Die openpilot‑Längsregelung könnte in einem zukünftigen Update kommen." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"openpilot erfordert, dass das Gerät innerhalb von 4° nach links oder rechts " +"und innerhalb von 5° nach oben oder 9° nach unten montiert ist." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "rechts" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "unbegrenzt" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "oben" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "Aktuell, zuletzt geprüft: nie" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "Aktuell, zuletzt geprüft: {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "Update verfügbar" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} WARNUNG" +msgstr[1] "{} WARNUNGEN" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "vor {} Tag" +msgstr[1] "vor {} Tagen" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "vor {} Stunde" +msgstr[1] "vor {} Stunden" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "vor {} Minute" +msgstr[1] "vor {} Minuten" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "{} Segment Ihrer Fahrten ist bisher im Trainingsdatensatz." +msgstr[1] "{} Segmente Ihrer Fahrten sind bisher im Trainingsdatensatz." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ ABONNIERT" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Firehose‑Modus 🔥" diff --git a/selfdrive/ui/translations/app_en.po b/selfdrive/ui/translations/app_en.po new file mode 100644 index 0000000000..6fbb537aff --- /dev/null +++ b/selfdrive/ui/translations/app_en.po @@ -0,0 +1,1207 @@ +# English translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-21 18:18-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " Steering torque response calibration is complete." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " Steering torque response calibration is {}% complete." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " Your device is pointed {:.1f}° {} and {:.1f}° {}." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 year of drive storage" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "24/7 LTE connectivity" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "

Steering lag calibration is complete." + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

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

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

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "openpilot longitudinal control may come in a future update." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "right" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "unmetered" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "up" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "up to date, last checked never" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "up to date, last checked {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "update available" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} ALERT" +msgstr[1] "{} ALERTS" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{} day ago" +msgstr[1] "{} days ago" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{} hour ago" +msgstr[1] "{} hours ago" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{} minute ago" +msgstr[1] "{} minutes ago" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "{} segment of your driving is in the training dataset so far." +msgstr[1] "{} segments of your driving is in the training dataset so far." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ SUBSCRIBED" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Firehose Mode 🔥" diff --git a/selfdrive/ui/translations/app_es.po b/selfdrive/ui/translations/app_es.po new file mode 100644 index 0000000000..59b9e6dfdb --- /dev/null +++ b/selfdrive/ui/translations/app_es.po @@ -0,0 +1,1225 @@ +# Spanish translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-20 16:35-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " La calibración de respuesta de par de dirección está completa." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " La calibración de respuesta de par de dirección está {}% completa." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " Tu dispositivo está orientado {:.1f}° {} y {:.1f}° {}." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 año de almacenamiento de conducción" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "Conectividad LTE 24/7" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"ADVERTENCIA: el control longitudinal de openpilot está en alpha para este " +"coche y deshabilitará el Frenado Automático de Emergencia (AEB).

En este coche, openpilot usa por defecto el ACC integrado del " +"coche en lugar del control longitudinal de openpilot. Activa esto para " +"cambiar al control longitudinal de openpilot. Se recomienda activar el modo " +"Experimental al habilitar el control longitudinal de openpilot (alpha)." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

Steering lag calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "ACTIVO" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB (Android Debug Bridge) permite conectar tu dispositivo por USB o por la " +"red. Consulta https://docs.comma.ai/how-to/connect-to-comma para más " +"información." + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "AÑADIR" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "Reconocer actuación excesiva" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "Agresivo" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "Aceptar" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "Supervisión del conductor siempre activa" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"Se puede probar una versión alpha del control longitudinal de openpilot, " +"junto con el modo Experimental, en ramas que no son de lanzamiento." + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "¿Seguro que quieres apagar?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "¿Seguro que quieres reiniciar?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "¿Seguro que quieres restablecer la calibración?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "¿Seguro que quieres desinstalar?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "Atrás" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "Hazte miembro de comma prime en connect.comma.ai" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "" +"Añade connect.comma.ai a tu pantalla de inicio para usarlo como una app" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "CAMBIAR" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "COMPROBAR" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "MODO CHILL ACTIVADO" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "CONECTAR" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "CONECTAR" + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "Cambiar idioma" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "" +" Cambiar esta configuración reiniciará openpilot si el coche está encendido." + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "" +"Haz clic en \"añadir nuevo dispositivo\" y escanea el código QR de la derecha" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "Cerrar" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "Versión actual" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "DESCARGAR" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "Rechazar" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "Rechazar, desinstalar openpilot" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "Desarrollador" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "Dispositivo" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "Desactivar con el pedal del acelerador" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "Desactivar para apagar" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "Desactivar para reiniciar" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "Desactivar para restablecer la calibración" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "Mostrar la velocidad en km/h en lugar de mph." + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "ID del dongle" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "Descargar" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "Cámara del conductor" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "Estilo de conducción" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "ERROR" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "MODO EXPERIMENTAL ACTIVADO" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "Activar" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "Activar ADB" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "Activar advertencias de salida de carril" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "Activar openpilot" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "Activar SSH" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "Activar advertencias de salida de carril" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "" +"Activar la supervisión del conductor incluso cuando openpilot no esté " +"activado." + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "Activar openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"Activa el interruptor de control longitudinal de openpilot (alpha) para " +"permitir el modo Experimental." + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "Introduce tu nombre de usuario de GitHub" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "Modo experimental" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"El modo experimental no está disponible actualmente en este coche, ya que se " +"usa el ACC de fábrica para el control longitudinal." + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "" + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "Finalizar configuración" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Modo Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"Para la máxima efectividad, lleva tu dispositivo al interior y conéctalo " +"semanalmente a un buen adaptador USB‑C y Wi‑Fi.\n" +"\n" +"El Modo Firehose también puede funcionar mientras conduces si está conectado " +"a un hotspot o a una SIM ilimitada.\n" +"\n" +"\n" +"Preguntas frecuentes\n" +"\n" +"¿Importa cómo o dónde conduzco? No, conduce como normalmente lo harías.\n" +"\n" +"¿Se suben todos mis segmentos en el Modo Firehose? No, seleccionamos un " +"subconjunto de tus segmentos.\n" +"\n" +"¿Qué es un buen adaptador USB‑C? Cualquier cargador rápido de teléfono o " +"laptop sirve.\n" +"\n" +"¿Importa qué software ejecuto? Sí, solo openpilot upstream (y forks " +"particulares) pueden usarse para entrenamiento." + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "BUENO" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "Ve a https://connect.comma.ai en tu teléfono" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "ALTO" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "Red" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "INACTIVO: conéctate a una red sin límites" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "INSTALAR" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "Instalar actualización" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "Modo de depuración de joystick" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "CARGANDO" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "Modo de maniobra longitudinal" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "MÁX" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" +"Maximiza tus cargas de datos de entrenamiento para mejorar los modelos de " +"conducción de openpilot." + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "NO" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "Red" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "No se encontraron claves SSH" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "No se encontraron claves SSH para el usuario '{username}'" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "No hay notas de versión disponibles." + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "SIN CONEXIÓN" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "OK" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "EN LÍNEA" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "Abrir" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "EMPAREJAR" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "VISTA PREVIA" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "FUNCIONES PRIME:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "Emparejar dispositivo" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "Emparejar dispositivo" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "Empareja tu dispositivo con tu cuenta de comma" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"Empareja tu dispositivo con comma connect (connect.comma.ai) y reclama tu " +"oferta de comma prime." + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "Conéctate a Wi‑Fi para completar el emparejamiento inicial" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "Apagar" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"Previsualiza la cámara hacia el conductor para asegurarte de que la " +"supervisión del conductor tenga buena visibilidad. (el vehículo debe estar " +"apagado)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "Error de código QR" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "ELIMINAR" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "RESTABLECER" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "REVISAR" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "Reiniciar" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "Reiniciar dispositivo" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "Reiniciar y actualizar" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"Recibe alertas para volver al carril cuando tu vehículo se desvíe sobre una " +"línea de carril detectada sin la direccional activada mientras conduces a " +"más de 31 mph (50 km/h)." + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "Grabar y subir cámara del conductor" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "Grabar y subir audio del micrófono" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"Grabar y almacenar audio del micrófono mientras conduces. El audio se " +"incluirá en el video de la dashcam en comma connect." + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "Reglamentario" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "Relajado" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "Acceso remoto" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "Capturas remotas" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "Se agotó el tiempo de espera de la solicitud" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "Restablecer" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "Restablecer calibración" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "Revisar guía de entrenamiento" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "Revisa las reglas, funciones y limitaciones de openpilot" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "" + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "Selecciona un idioma" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "Número de serie" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "Posponer actualización" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "Software" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "Estándar" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"Se recomienda Estándar. En modo agresivo, openpilot seguirá más de cerca a " +"los coches delanteros y será más agresivo con el acelerador y el freno. En " +"modo relajado, openpilot se mantendrá más lejos de los coches delanteros. En " +"coches compatibles, puedes cambiar entre estas personalidades con el botón " +"de distancia del volante." + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "Sistema sin respuesta" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "TOME EL CONTROL INMEDIATAMENTE" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "TEMP" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "Interruptores" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "DESINSTALAR" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "ACTUALIZAR" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "Desinstalar" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "Desconocido" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "Las actualizaciones solo se descargan cuando el coche está apagado." + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "Mejorar ahora" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"Sube datos de la cámara orientada al conductor y ayuda a mejorar el " +"algoritmo de supervisión del conductor." + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "Usar sistema métrico" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"Usa el sistema openpilot para control de crucero adaptativo y asistencia de " +"mantenimiento de carril. Tu atención se requiere en todo momento para usar " +"esta función." + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "VEHÍCULO" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "VER" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "Esperando para iniciar" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"Advertencia: Esto otorga acceso SSH a todas las claves públicas en tu " +"configuración de GitHub. Nunca introduzcas un nombre de usuario de GitHub " +"que no sea el tuyo. Un empleado de comma NUNCA te pedirá que agregues su " +"nombre de usuario de GitHub." + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "Bienvenido a openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "" +"Cuando está activado, al presionar el pedal del acelerador se desactivará " +"openpilot." + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "Debes aceptar los Términos y Condiciones para poder usar openpilot." + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"Debes aceptar los Términos y Condiciones para usar openpilot. Lee los " +"términos más recientes en https://comma.ai/terms antes de continuar." + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "iniciando cámara" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "abajo" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "Error al buscar actualizaciones" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "km/h" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "izquierda" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "mph" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "nunca" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "ahora" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "Control longitudinal de openpilot (Alpha)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot no disponible" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot conduce por defecto en modo chill. El modo Experimental habilita " +"funciones de nivel alpha que no están listas para el modo chill. Las " +"funciones experimentales se enumeran a continuación:

Control " +"longitudinal de extremo a extremo


Deja que el modelo de conducción " +"controle el acelerador y los frenos. openpilot conducirá como piensa que lo " +"haría un humano, incluyendo detenerse en luces rojas y señales de alto. Dado " +"que el modelo decide la velocidad a la que conducir, la velocidad " +"establecida solo actuará como límite superior. Esta es una función de " +"calidad alpha; se deben esperar errores.

Nueva visualización de " +"conducción


La visualización de conducción hará la transición a la " +"cámara gran angular orientada a la carretera a bajas velocidades para " +"mostrar mejor algunos giros. El logotipo del modo Experimental también se " +"mostrará en la esquina superior derecha." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +" Cambiar esta configuración reiniciará openpilot si el coche está encendido." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot aprende a conducir observando a humanos, como tú, conducir.\n" +"\n" +"El Modo Firehose te permite maximizar tus cargas 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." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "" +"El control longitudinal de openpilot podría llegar en una actualización " +"futura." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"openpilot requiere que el dispositivo esté montado dentro de 4° a izquierda " +"o derecha y dentro de 5° hacia arriba o 9° hacia abajo." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "derecha" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "arriba" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "actualizado, última comprobación: nunca" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "actualizado, última comprobación: {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "actualización disponible" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} ALERTA" +msgstr[1] "{} ALERTAS" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "hace {} día" +msgstr[1] "hace {} días" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "hace {} hora" +msgstr[1] "hace {} horas" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "hace {} minuto" +msgstr[1] "hace {} minutos" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "" +"{} segmento de tu conducción está en el conjunto de entrenamiento hasta " +"ahora." +msgstr[1] "" +"{} segmentos de tu conducción están en el conjunto de entrenamiento hasta " +"ahora." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ SUSCRITO" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Modo Firehose 🔥" diff --git a/selfdrive/ui/translations/app_fr.po b/selfdrive/ui/translations/app_fr.po new file mode 100644 index 0000000000..f883d4d485 --- /dev/null +++ b/selfdrive/ui/translations/app_fr.po @@ -0,0 +1,1230 @@ +# French translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-20 18:19-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 an de stockage de trajets" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "Connexion LTE 24/7" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"ATTENTION : le contrôle longitudinal openpilot est en alpha pour cette " +"voiture et désactivera le freinage d'urgence automatique (AEB).

Sur cette voiture, openpilot utilise par défaut le régulateur de " +"vitesse adaptatif intégré au véhicule 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." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

Steering lag calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "ACTIF" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB (Android Debug Bridge) permet de connecter votre appareil via USB ou via " +"le réseau. Voir https://docs.comma.ai/how-to/connect-to-comma pour plus " +"d'informations." + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "AJOUTER" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "Accuser réception d'actionnement excessif" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "Agressif" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "Accepter" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "Surveillance continue du conducteur" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"Une version alpha du contrôle longitudinal openpilot peut être testée, avec " +"le mode expérimental, sur des branches non publiées." + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "Êtes-vous sûr de vouloir éteindre ?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "Êtes-vous sûr de vouloir redémarrer ?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "Êtes-vous sûr de vouloir réinitialiser la calibration ?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "Êtes-vous sûr de vouloir désinstaller ?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "Retour" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "Devenez membre comma prime sur connect.comma.ai" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "" +"Ajoutez connect.comma.ai à votre écran d'accueil pour l'utiliser comme une " +"application" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "CHANGER" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "VÉRIFIER" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "MODE CHILL ACTIVÉ" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "CONNECTER" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "CONNECTER" + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "Changer la langue" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "" +" La modification de ce réglage redémarrera openpilot si la voiture est sous " +"tension." + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "Cliquez sur \"add new device\" et scannez le code QR à droite" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "Fermer" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "Version actuelle" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "TÉLÉCHARGER" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "Refuser" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "Refuser, désinstaller openpilot" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "Développeur" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "Appareil" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "Désengager à l'appui sur l'accélérateur" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "Désengager pour éteindre" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "Désengager pour redémarrer" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "Désengager pour réinitialiser la calibration" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "Afficher la vitesse en km/h au lieu de mph." + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "ID du dongle" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "Télécharger" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "Caméra conducteur" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "Personnalité de conduite" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "ERREUR" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "MODE EXPÉRIMENTAL ACTIVÉ" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "Activer" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "Activer ADB" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "Activer les alertes de sortie de voie" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "Activer openpilot" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "Activer SSH" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "Activer les alertes de sortie de voie" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "" +"Activer la surveillance du conducteur même lorsque openpilot n'est pas " +"engagé." + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "Activer openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"Activez l'option de contrôle longitudinal openpilot (alpha) pour autoriser " +"le mode expérimental." + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "Entrez votre nom d'utilisateur GitHub" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "Mode expérimental" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"Le mode expérimental est actuellement indisponible sur cette voiture car " +"l'ACC d'origine est utilisé pour le contrôle longitudinal." + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "" + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "Terminer la configuration" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Mode Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"Pour une efficacité maximale, rentrez votre appareil et connectez-le chaque " +"semaine à un bon adaptateur USB-C et au Wi‑Fi.\n" +"\n" +"Le Mode Firehose peut aussi fonctionner pendant que vous conduisez si vous " +"êtes connecté à un hotspot ou à une carte SIM illimitée.\n" +"\n" +"\n" +"Foire aux questions\n" +"\n" +"Est-ce que la manière ou l'endroit où je conduis compte ? Non, conduisez " +"normalement.\n" +"\n" +"Tous mes segments sont-ils récupérés en Mode Firehose ? Non, nous récupérons " +"de façon sélective un sous-ensemble de vos segments.\n" +"\n" +"Quel est un bon adaptateur USB-C ? Tout chargeur rapide de téléphone ou " +"d'ordinateur portable convient.\n" +"\n" +"Le logiciel utilisé importe-t-il ? Oui, seul openpilot amont (et certains " +"forks) peut être utilisé pour l'entraînement." + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "BON" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "Allez sur https://connect.comma.ai sur votre téléphone" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "ÉLEVÉ" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "Réseau" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "INACTIF : connectez-vous à un réseau non limité" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "INSTALLER" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "Installer la mise à jour" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "Mode débogage joystick" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "CHARGEMENT" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "Mode de manœuvre longitudinale" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "MAX" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" +"Maximisez vos envois de données d'entraînement pour améliorer les modèles de " +"conduite d'openpilot." + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "NON" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "Réseau" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "Aucune clé SSH trouvée" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "Aucune clé SSH trouvée pour l'utilisateur '{username}'" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "Aucune note de version disponible." + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "HORS LIGNE" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "OK" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "EN LIGNE" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "Ouvrir" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "ASSOCIER" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "APERÇU" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "FONCTIONNALITÉS PRIME :" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "Associer l'appareil" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "Associer l'appareil" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "Associez votre appareil à votre compte comma" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"Associez votre appareil à comma connect (connect.comma.ai) et réclamez votre " +"offre comma prime." + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "Veuillez vous connecter au Wi‑Fi pour terminer l'association initiale" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "Éteindre" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"Prévisualisez la caméra orientée conducteur pour vous assurer que la " +"surveillance du conducteur a une bonne visibilité. (le véhicule doit être " +"éteint)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "Erreur de code QR" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "SUPPRIMER" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "RÉINITIALISER" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "CONSULTER" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "Redémarrer" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "Redémarrer l'appareil" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "Redémarrer et mettre à jour" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"Recevez des alertes pour revenir dans la voie lorsque votre véhicule dépasse " +"une ligne de voie détectée sans clignotant activé en roulant au-delà de 31 " +"mph (50 km/h)." + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "Enregistrer et téléverser la caméra conducteur" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "Enregistrer et téléverser l'audio du microphone" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"Enregistrer et stocker l'audio du microphone pendant la conduite. L'audio " +"sera inclus dans la vidéo dashcam dans comma connect." + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "Réglementaire" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "Détendu" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "Accès à distance" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "Captures à distance" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "Délai de la requête dépassé" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "Réinitialiser" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "Réinitialiser la calibration" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "Consulter le guide d'entraînement" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "Consultez les règles, fonctionnalités et limitations d'openpilot" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "" + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "Numéro de série" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "Reporter la mise à jour" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "Logiciel" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "Standard" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"Le mode standard est recommandé. En mode agressif, openpilot suivra les " +"véhicules de tête de plus près et sera plus agressif avec l'accélérateur et " +"le frein. En mode détendu, openpilot restera plus éloigné des véhicules de " +"tête. Sur les voitures compatibles, vous pouvez parcourir ces personnalités " +"avec le bouton de distance du volant." + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "Système non réactif" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "REPRENEZ IMMÉDIATEMENT LE CONTRÔLE" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "TEMPÉRATURE" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "Options" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "DÉSINSTALLER" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "METTRE À JOUR" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "Désinstaller" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "Inconnu" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "" +"Les mises à jour ne sont téléchargées que lorsque la voiture est éteinte." + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "Mettre à niveau maintenant" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"Téléverser les données de la caméra orientée conducteur et aider à améliorer " +"l'algorithme de surveillance du conducteur." + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "Utiliser le système métrique" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"Utilisez le système openpilot pour l'ACC et l'assistance au maintien de " +"voie. Votre attention est requise en permanence pour utiliser cette " +"fonctionnalité." + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "VÉHICULE" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "VOIR" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "En attente de démarrage" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"Avertissement : Ceci accorde un accès SSH à toutes les clés publiques dans " +"vos paramètres GitHub. N'entrez jamais un nom d'utilisateur GitHub autre que " +"le vôtre. Un employé comma ne vous demandera JAMAIS d'ajouter son nom " +"d'utilisateur GitHub." + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "Bienvenue sur openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "" +"Lorsque activé, appuyer sur la pédale d'accélérateur désengagera openpilot." + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "Vous devez accepter les conditions générales pour utiliser openpilot." + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"Vous devez accepter les conditions générales pour utiliser openpilot. Lisez " +"les dernières conditions sur https://comma.ai/terms avant de continuer." + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "démarrage de la caméra" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "échec de la vérification de mise à jour" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "km/h" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "mph" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "jamais" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "maintenant" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "Contrôle longitudinal openpilot (Alpha)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot indisponible" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot roule par défaut en mode chill. Le mode expérimental active des " +"fonctionnalités de niveau alpha qui ne sont pas prêtes pour le mode chill. " +"Les fonctionnalités expérimentales sont listées ci‑dessous:

Contrôle " +"longitudinal de bout en bout


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 décide de la vitesse à adopter, la vitesse réglée n'agira que " +"comme une limite supérieure. C'est une fonctionnalité de qualité alpha ; des " +"erreurs sont à prévoir.

Nouvelle visualisation de conduite


La " +"visualisation passera à la caméra grand angle orientée route à basse vitesse " +"pour mieux montrer certains virages. Le logo du mode expérimental sera " +"également affiché en haut à droite." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +" La modification de ce réglage redémarrera openpilot si la voiture est sous " +"tension." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot apprend à conduire en regardant des humains, comme vous, " +"conduire.\n" +"\n" +"Le Mode Firehose vous permet de maximiser vos envois de données " +"d'entraînement pour améliorer les modèles de conduite d'openpilot. Plus de " +"données signifie des modèles plus grands, ce qui signifie un meilleur Mode " +"expérimental." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "" +"Le contrôle longitudinal openpilot pourra arriver dans une future mise à " +"jour." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"openpilot exige que l'appareil soit monté à moins de 4° à gauche ou à droite " +"et à moins de 5° vers le haut ou 9° vers le bas." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "à jour, dernière vérification jamais" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "à jour, dernière vérification {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "mise à jour disponible" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} ALERTE" +msgstr[1] "{} ALERTES" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "il y a {} jour" +msgstr[1] "il y a {} jours" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "il y a {} heure" +msgstr[1] "il y a {} heures" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "il y a {} minute" +msgstr[1] "il y a {} minutes" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "" +"{} segment de votre conduite est dans l'ensemble d'entraînement jusqu'à " +"présent." +msgstr[1] "" +"{} segments de votre conduite sont dans l'ensemble d'entraînement jusqu'à " +"présent." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ ABONNÉ" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Mode Firehose 🔥" diff --git a/selfdrive/ui/translations/app_ja.po b/selfdrive/ui/translations/app_ja.po new file mode 100644 index 0000000000..ca8aac1515 --- /dev/null +++ b/selfdrive/ui/translations/app_ja.po @@ -0,0 +1,1197 @@ +# Japanese translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-22 16:32-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " ステアリングトルク応答のキャリブレーションが完了しました。" + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " ステアリングトルク応答のキャリブレーションは{}%完了しました。" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " デバイスは{:.1f}°{}、{:.1f}°{}の向きです。" + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "走行データを1年間保存" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "24時間365日のLTE接続" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"警告: この車におけるopenpilotの縦制御はアルファ版であり、自動緊急ブレーキ" +"(AEB)を無効にします。

この車では、openpilotは縦制御として" +"openpilotではなく車両の内蔵ACCを既定で使用します。openpilotの縦制御に切り替え" +"るにはこの設定を有効にしてください。openpilot縦制御アルファを有効にする場合は" +"実験モードの有効化を推奨します。この設定を変更すると、車が起動中の場合は" +"openpilotが再起動します。" + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "

ステアリング遅延のキャリブレーションが完了しました。" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

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

ステアリング遅延のキャリブレーションは{}%完了しました。" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "アクティブ" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB(Android Debug Bridge)を使用すると、USBまたはネットワーク経由でデバイス" +"に接続できます。詳しくは https://docs.comma.ai/how-to/connect-to-comma を参照" +"してください。" + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "追加" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "APN設定" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "過度な作動を承認" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "詳細設定" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "アグレッシブ" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "同意する" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "常時ドライバーモニタリング" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"openpilotの縦制御アルファ版は、実験モードと併せて非リリースブランチでテストで" +"きます。" + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "本当に電源をオフにしますか?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "本当に再起動しますか?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "本当にキャリブレーションをリセットしますか?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "本当にアンインストールしますか?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "戻る" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "connect.comma.aiで comma prime に加入" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "connect.comma.aiをホーム画面に追加してアプリのように使いましょう" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "変更" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "確認" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "チルモードON" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "接続" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "接続中..." + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "キャンセル" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "従量課金の携帯回線" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "言語を変更" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "車が起動中の場合、この設定を変更するとopenpilotが再起動します。" + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "\"add new device\"を押して右側のQRコードをスキャン" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "閉じる" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "現在のバージョン" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "ダウンロード" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "拒否する" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "拒否してopenpilotをアンインストール" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "開発者" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "デバイス" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "アクセルで解除" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "解除して電源オフ" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "解除して再起動" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "解除してキャリブレーションをリセット" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "速度をmphではなくkm/hで表示します。" + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "ドングルID" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "ダウンロード" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "ドライバーカメラ" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "走行性格" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "編集" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "エラー" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "実験モードON" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "有効化" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "ADBを有効化" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "車線逸脱警報を有効化" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "ローミングを有効化" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "SSHを有効化" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "テザリングを有効化" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "openpilotが未作動でもドライバーモニタリングを有効にします。" + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "openpilotを有効化" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"openpilot縦制御(アルファ)のトグルを有効にすると実験モードが使用できます。" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "APNを入力" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "SSIDを入力" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "新しいテザリングのパスワードを入力" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "パスワードを入力" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "GitHubユーザー名を入力" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "エラー" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "実験モード" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"この車では縦制御に純正ACCを使用するため、現在実験モードは利用できません。" + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "削除中..." + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "セットアップを完了" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Firehoseモード" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"最大限の効果を得るため、デバイスを屋内に持ち込み、週に一度は品質の良いUSB-Cア" +"ダプターとWi‑Fiに接続してください。\n" +"\n" +"Firehoseモードは、ホットスポットや無制限SIMに接続していれば走行中でも動作しま" +"す。\n" +"\n" +"\n" +"よくある質問\n" +"\n" +"運転の仕方や場所は関係ありますか? いいえ。普段どおりに運転してください。\n" +"\n" +"Firehoseモードではすべてのセグメントが取得されますか? いいえ。セグメントの一" +"部を選択的に取得します。\n" +"\n" +"良いUSB‑Cアダプターとは? 高速なスマホまたはノートPC用充電器で問題ありませ" +"ん。\n" +"\n" +"どのソフトウェアを使うかは重要ですか? はい。学習に使えるのは上流のopenpilot" +"(および特定のフォーク)のみです。" + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "削除" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "Wi‑Fiネットワーク「{}」を削除しますか?" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "良好" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "スマートフォンで https://connect.comma.ai にアクセス" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "高温" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "非公開ネットワーク" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "非アクティブ:非従量のネットワークに接続してください" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "インストール" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "IPアドレス" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "アップデートをインストール" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "ジョイスティックデバッグモード" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "読み込み中" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "縦制御マヌーバーモード" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "最大" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" +"学習データのアップロードを最大化してopenpilotの運転モデルを改善しましょう。" + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "該当なし" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "いいえ" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "ネットワーク" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "SSH鍵が見つかりません" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "ユーザー'{}'のSSH鍵が見つかりません" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "リリースノートはありません。" + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "オフライン" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "OK" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "オンライン" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "開く" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "ペアリング" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "プレビュー" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "prime の特典:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "デバイスをペアリング" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "デバイスをペアリング" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "デバイスをあなたの comma アカウントにペアリング" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"デバイスを comma connect(connect.comma.ai)とペアリングして、comma prime 特" +"典を受け取りましょう。" + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "初回ペアリングを完了するにはWi‑Fiに接続してください" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "電源オフ" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "従量課金のWi‑Fi接続時は大きなデータのアップロードを抑制" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "従量課金の携帯回線接続時は大きなデータのアップロードを抑制" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"ドライバー向きカメラのプレビューでモニタリングの視界を確認します。(車両は停" +"止状態である必要があります)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "QRコードエラー" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "削除" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "リセット" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "確認" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "再起動" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "デバイスを再起動" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "再起動して更新" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"時速31mph(50km/h)を超えて走行中にウインカーを出さず検出された車線を外れた場" +"合、車線内に戻るよう警告を受け取ります。" + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "ドライバーカメラを記録してアップロード" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "マイク音声を記録してアップロード" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"走行中にマイク音声を記録・保存します。音声は comma connect のドライブレコー" +"ダー動画に含まれます。" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "規制情報" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "リラックス" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "リモートアクセス" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "リモートスナップショット" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "リクエストがタイムアウトしました" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "リセット" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "キャリブレーションをリセット" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "トレーニングガイドを確認" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "openpilotのルール、機能、制限を確認" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "選択" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "SSH鍵" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "Wi‑Fiネットワークを検索中..." + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "選択" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "ブランチを選択" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "言語を選択" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "シリアル" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "更新を後で通知" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "ソフトウェア" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "スタンダード" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"標準を推奨します。アグレッシブでは前走車に近づき、加減速も積極的になります。" +"リラックスでは前走車との距離を保ちます。対応車種ではステアリングの車間ボタン" +"でこれらの性格を切り替えられます。" + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "システムが応答しません" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "すぐに手動介入してください" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "温度" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "対象ブランチ" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "テザリングのパスワード" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "トグル" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "アンインストール" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "更新" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "アンインストール" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "不明" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "アップデートは車両の電源が切れている間のみダウンロードされます。" + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "今すぐアップグレード" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"ドライバー向きカメラのデータをアップロードしてモニタリングアルゴリズムの改善" +"に協力してください。" + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "メートル法を使用" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"ACCと車線維持支援にopenpilotを使用します。本機能の使用中は常に注意が必要で" +"す。" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "車両" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "表示" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "開始待機中" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"警告: これはGitHub設定内のすべての公開鍵にSSHアクセスを与えます。自分以外の" +"GitHubユーザー名を絶対に入力しないでください。comma の従業員が自分のGitHub" +"ユーザー名を追加するよう求めることは決してありません。" + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "openpilotへようこそ" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "有効にすると、アクセルを踏むとopenpilotが解除されます。" + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "Wi‑Fiネットワーク(従量課金)" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "パスワードが違います" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "openpilotを使用するには、利用規約に同意する必要があります。" + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"openpilotを使用するには利用規約に同意する必要があります。続行する前に " +"https://comma.ai/terms の最新の規約をお読みください。" + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "カメラを起動中" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "既定" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "下" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "アップデートの確認に失敗しました" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "「{}」向け" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "km/h" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "自動設定の場合は空欄のままにしてください" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "左" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "従量" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "mph" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "なし" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "今" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "openpilot 縦制御(アルファ)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilotは利用できません" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilotは既定でチルモードで走行します。実験モードでは、チルモードにはまだ準" +"備ができていないアルファレベルの機能が有効になります。実験的な機能は以下のと" +"おりです:

エンドツーエンド縦制御


運転モデルがアクセルとブレー" +"キを制御します。openpilotは人間のように走行し、赤信号や一時停止でも停止しま" +"す。走行速度は運転モデルが決めるため、設定速度は上限としてのみ機能します。こ" +"れはアルファ品質の機能であり、誤動作が発生する可能性があります。

新し" +"い運転ビジュアライゼーション


低速時には道路向きの広角カメラに切り替わ" +"り、一部の曲がりをより良く表示します。画面右上には実験モードのロゴも表示され" +"ます。" + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +"openpilotは継続的にキャリブレーションを行っており、リセットが必要になることは" +"稀です。車が起動中にキャリブレーションをリセットするとopenpilotが再起動しま" +"す。" + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilotは、あなたのような人間の運転を見て運転を学習します。\n" +"\n" +"Firehoseモードを使うと、学習データのアップロードを最大化してopenpilotの運転モ" +"デルを改善できます。データが増えるほどモデルが大きくなり、実験モードがより良" +"くなります。" + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "openpilotの縦制御は将来のアップデートで提供される可能性があります。" + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"openpilotでは、デバイスの取り付け角度が左右±4°、上方向5°以内、下方向9°以内で" +"ある必要があります。" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "右" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "非従量" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "上" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "最新です。最終確認: なし" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "最新です。最終確認: {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "更新があります" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{}件のアラート" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{}日前" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{}時間前" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{}分前" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "" +"これまでにあなたの走行の{}セグメントが学習データセットに含まれています。" + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ 登録済み" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Firehoseモード 🔥" diff --git a/selfdrive/ui/translations/app_ko.po b/selfdrive/ui/translations/app_ko.po new file mode 100644 index 0000000000..5a3e891b87 --- /dev/null +++ b/selfdrive/ui/translations/app_ko.po @@ -0,0 +1,1190 @@ +# Korean translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-22 16:32-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " 스티어링 토크 응답 보정이 완료되었습니다." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " 스티어링 토크 응답 보정이 {}% 완료되었습니다." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " 장치는 {:.1f}° {} 및 {:.1f}° {} 방향을 가리키고 있습니다." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "주행 데이터 1년 보관" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "연중무휴 LTE 연결" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"경고: 이 차량에서 openpilot의 종방향 제어는 알파 버전이며 자동 긴급 제동" +"(AEB)을 비활성화합니다.

이 차량에서는 openpilot 종방향 제어 대신 " +"차량 내장 ACC가 기본으로 사용됩니다. openpilot 종방향 제어로 전환하려면 이 설" +"정을 켜세요. 종방향 제어 알파를 켤 때는 실험 모드 사용을 권장합니다. 차량 전" +"원이 켜져 있는 경우 이 설정을 변경하면 openpilot이 재시작됩니다." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "

스티어링 지연 보정이 완료되었습니다." + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

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

스티어링 지연 보정이 {}% 완료되었습니다." + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "활성" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB(Android Debug Bridge)를 사용하면 USB 또는 네트워크로 장치에 연결할 수 있" +"습니다. 자세한 내용은 https://docs.comma.ai/how-to/connect-to-comma 를 참고하" +"세요." + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "추가" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "APN 설정" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "과도한 작동을 확인" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "고급" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "공격적" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "동의" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "항상 켜짐 운전자 모니터링" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"openpilot 종방향 제어 알파 버전은 실험 모드와 함께 비릴리스 브랜치에서 테스트" +"할 수 있습니다." + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "정말 전원을 끄시겠습니까?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "정말 재시작하시겠습니까?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "정말 보정을 재설정하시겠습니까?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "정말 제거하시겠습니까?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "뒤로" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "connect.comma.ai에서 comma prime 회원이 되세요" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "connect.comma.ai를 홈 화면에 추가하여 앱처럼 사용하세요" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "변경" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "확인" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "칠 모드 켜짐" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "연결" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "연결 중..." + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "취소" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "종량제 셀룰러" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "언어 변경" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "차량 전원이 켜져 있으면 이 설정을 변경할 때 openpilot이 재시작됩니다." + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "\"add new device\"를 눌러 오른쪽의 QR 코드를 스캔하세요" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "닫기" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "현재 버전" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "다운로드" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "거부" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "거부하고 openpilot 제거" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "개발자" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "장치" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "가속 페달로 해제" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "해제 후 전원 끄기" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "해제 후 재시작" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "해제 후 보정 재설정" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "속도를 mph 대신 km/h로 표시합니다." + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "동글 ID" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "다운로드" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "운전자 카메라" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "주행 성향" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "편집" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "오류" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "실험 모드 켜짐" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "사용" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "ADB 사용" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "차선 이탈 경고 사용" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "로밍 사용" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "SSH 사용" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "테더링 사용" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "openpilot이 작동 중이 아닐 때도 운전자 모니터링을 사용합니다." + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "openpilot 사용" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "실험 모드를 사용하려면 openpilot 종방향 제어(알파) 토글을 켜세요." + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "APN 입력" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "SSID 입력" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "새 테더링 비밀번호 입력" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "비밀번호 입력" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "GitHub 사용자 이름 입력" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "오류" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "실험 모드" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"이 차량은 종방향 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습" +"니다." + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "삭제 중..." + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "설정 완료" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Firehose 모드" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"최대의 효과를 위해 주 1회는 장치를 실내로 가져와 품질 좋은 USB‑C 어댑터와 " +"Wi‑Fi에 연결하세요.\n" +"\n" +"핫스팟이나 무제한 SIM에 연결되어 있다면 주행 중에도 Firehose 모드가 동작합니" +"다.\n" +"\n" +"\n" +"자주 묻는 질문\n" +"\n" +"어떻게, 어디서 운전하는지가 중요한가요? 아니요. 평소처럼 운전하세요.\n" +"\n" +"Firehose 모드에서 모든 세그먼트가 가져가지나요? 아니요. 일부 세그먼트만 선택" +"적으로 가져갑니다.\n" +"\n" +"좋은 USB‑C 어댑터는 무엇인가요? 빠른 휴대폰 또는 노트북 충전기면 충분합니" +"다.\n" +"\n" +"어떤 소프트웨어를 실행하는지가 중요한가요? 예. 학습에는 업스트림 " +"openpilot(및 일부 포크)만 사용할 수 있습니다." + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "삭제" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "Wi‑Fi 네트워크 \"{}\"를 삭제하시겠습니까?" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "양호" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "휴대폰에서 https://connect.comma.ai 에 접속하세요" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "높음" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "숨겨진 네트워크" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "비활성: 비종량제 네트워크에 연결하세요" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "설치" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "IP 주소" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "업데이트 설치" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "조이스틱 디버그 모드" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "로딩 중" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "종방향 매뉴버 모드" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "최대" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "학습 데이터 업로드를 최대화하여 openpilot의 주행 모델을 개선하세요." + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "해당 없음" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "아니오" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "네트워크" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "SSH 키를 찾을 수 없습니다" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "사용자 '{}'의 SSH 키를 찾을 수 없습니다" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "릴리스 노트가 없습니다." + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "오프라인" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "확인" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "온라인" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "열기" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "페어링" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "미리보기" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "prime 기능:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "장치 페어링" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "장치 페어링" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "장치를 귀하의 comma 계정에 페어링하세요" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"장치를 comma connect(connect.comma.ai)와 페어링하고 comma prime 혜택을 받으세" +"요." + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "초기 페어링을 완료하려면 Wi‑Fi에 연결하세요" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "전원 끄기" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "종량제 Wi‑Fi 연결 시 대용량 업로드 방지" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "종량제 셀룰러 연결 시 대용량 업로드 방지" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"운전자 모니터링의 가시성을 확인하기 위해 운전자 카메라를 미리 봅니다. (차량" +"은 꺼져 있어야 합니다)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "QR 코드 오류" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "제거" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "재설정" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "검토" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "재시작" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "장치 재시작" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "재시작 및 업데이트" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"시속 31mph(50km/h) 이상에서 방향지시등 없이 감지된 차선 밖으로 벗어나면 차선" +"으로 복귀하라는 경고를 받습니다." + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "운전자 카메라 기록 및 업로드" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "마이크 오디오 기록 및 업로드" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"주행 중 마이크 오디오를 기록하고 저장합니다. 오디오는 comma connect의 대시캠 " +"영상에 포함됩니다." + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "규제 정보" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "편안함" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "원격 액세스" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "원격 스냅샷" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "요청 시간이 초과되었습니다" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "재설정" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "보정 재설정" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "학습 가이드 검토" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "openpilot의 규칙, 기능 및 제한을 검토" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "선택" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "SSH 키" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "Wi‑Fi 네트워크 검색 중..." + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "선택" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "브랜치 선택" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "언어 선택" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "시리얼" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "업데이트 나중에 알림" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "소프트웨어" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "표준" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"표준을 권장합니다. 공격적 모드에서는 앞차를 더 가깝게 따라가고 가감속이 더 적" +"극적입니다. 편안함 모드에서는 앞차와 거리를 더 둡니다. 지원 차량에서는 스티어" +"링의 차간 버튼으로 이 성향들을 전환할 수 있습니다." + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "시스템 응답 없음" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "즉시 수동 조작하세요" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "온도" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "대상 브랜치" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "테더링 비밀번호" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "토글" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "제거" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "업데이트" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "제거" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "알 수 없음" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "업데이트는 차량 전원이 꺼져 있을 때만 다운로드됩니다." + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "지금 업그레이드" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"운전자 방향 카메라 데이터를 업로드하여 운전자 모니터링 알고리즘 개선에 도움" +"을 주세요." + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "미터법 사용" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"ACC 및 차선 유지 보조에 openpilot을 사용합니다. 이 기능을 사용할 때는 항상 주" +"의가 필요합니다." + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "차량" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "보기" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "시작 대기 중" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"경고: 이는 GitHub 설정의 모든 공개 키에 SSH 액세스를 부여합니다. 자신의 것이 " +"아닌 GitHub 사용자 이름을 절대 입력하지 마세요. comma 직원이 본인의 GitHub 사" +"용자 이름 추가를 요구하는 일은 결코 없습니다." + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "openpilot에 오신 것을 환영합니다" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "이 옵션을 켜면 가속 페달을 밟을 때 openpilot이 해제됩니다." + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "Wi‑Fi 네트워크 종량제" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "비밀번호가 올바르지 않습니다" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "openpilot을 사용하려면 약관에 동의해야 합니다." + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"openpilot을 사용하려면 약관에 동의해야 합니다. 계속하기 전에 https://comma." +"ai/terms 에서 최신 약관을 읽어주세요." + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "카메라 시작 중" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "기본값" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "아래" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "업데이트 확인 실패" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "\"{}\"용" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "km/h" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "자동 구성을 사용하려면 비워 두세요" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "왼쪽" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "종량제" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "mph" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "없음" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "지금" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "openpilot 종방향 제어(알파)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot 사용 불가" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot은 기본적으로 칠 모드로 주행합니다. 실험 모드를 사용하면 칠 모드에 " +"아직 준비되지 않은 알파 수준의 기능이 활성화됩니다. 실험 기능은 아래와 같습니" +"다:

엔드투엔드 종방향 제어


주행 모델이 가속과 제동을 제어합니" +"다. openpilot은 빨간 신호 및 정지 표지에서의 정지를 포함해 사람이 운전한다고 " +"판단하는 방식으로 주행합니다. 주행 속도는 모델이 결정하므로 설정 속도는 상한" +"으로만 동작합니다. 알파 품질 기능이므로 오작동이 발생할 수 있습니다.

" +"새로운 주행 시각화


저속에서는 도로 방향의 광각 카메라로 전환되어 일" +"부 회전을 더 잘 보여줍니다. 화면 오른쪽 위에는 실험 모드 로고도 표시됩니다." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +"openpilot은 지속적으로 보정을 진행하므로 재설정이 필요한 경우는 드뭅니다. 차" +"량 전원이 켜져 있을 때 보정을 재설정하면 openpilot이 재시작됩니다." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot은 당신과 같은 사람의 운전을 보며 운전을 학습합니다.\n" +"\n" +"Firehose 모드는 학습 데이터 업로드를 최대화하여 openpilot의 주행 모델을 개선" +"할 수 있게 해줍니다. 데이터가 많을수록 모델은 커지고, 실험 모드는 더 좋아집니" +"다." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "openpilot 종방향 제어는 향후 업데이트에서 제공될 수 있습니다." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "openpilot은 장치를 좌우 4°, 위쪽 5°, 아래쪽 9° 이내로 장착해야 합니다." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "오른쪽" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "비종량제" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "위" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "최신입니다. 마지막 확인: 없음" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "최신입니다. 마지막 확인: {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "업데이트 가능" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{}건의 알림" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{}일 전" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{}시간 전" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{}분 전" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "현재까지 귀하의 주행 {}세그먼트가 학습 데이터셋에 포함되었습니다." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ 구독됨" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Firehose 모드 🔥" diff --git a/selfdrive/ui/translations/app_pt-BR.po b/selfdrive/ui/translations/app_pt-BR.po new file mode 100644 index 0000000000..8a388d0da0 --- /dev/null +++ b/selfdrive/ui/translations/app_pt-BR.po @@ -0,0 +1,1216 @@ +# Language pt-BR translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-21 00:00-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: pt-BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " A calibração da resposta de torque da direção foi concluída." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " A calibração da resposta de torque da direção está {}% concluída." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " Seu dispositivo está apontado {:.1f}° {} e {:.1f}° {}." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 ano de armazenamento de condução" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "Conectividade LTE 24/7" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"AVISO: o controle longitudinal do openpilot está em alpha para este carro " +"e desativará a Frenagem Automática de Emergência (AEB).

Neste " +"carro, o openpilot usa por padrão o ACC integrado do carro em vez do " +"controle longitudinal do openpilot. Ative isto para alternar para o controle " +"longitudinal do openpilot. Recomenda-se ativar o Modo Experimental ao ativar " +"o controle longitudinal do openpilot em alpha." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

Steering lag calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "ATIVO" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB (Android Debug Bridge) permite conectar ao seu dispositivo via USB ou " +"pela rede. Veja https://docs.comma.ai/how-to/connect-to-comma para mais " +"informações." + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "ADICIONAR" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "Reconhecer Atuação Excessiva" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "Agressivo" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "Concordo" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "Monitoramento de Motorista Sempre Ativo" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"Uma versão alpha do controle longitudinal do openpilot pode ser testada, " +"junto com o Modo Experimental, em ramificações fora de release." + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "Tem certeza de que deseja desligar?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "Tem certeza de que deseja reiniciar?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "Tem certeza de que deseja redefinir a calibração?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "Tem certeza de que deseja desinstalar?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "Voltar" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "Torne-se membro comma prime em connect.comma.ai" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "Adicione connect.comma.ai à tela inicial para usá-lo como um app" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "ALTERAR" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "VERIFICAR" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "MODO CHILL ATIVO" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "CONECTAR" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "CONECTAR" + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "Alterar Idioma" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "" +" Alterar esta configuração reiniciará o openpilot se o carro estiver ligado." + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "Toque em \"adicionar novo dispositivo\" e escaneie o QR code à direita" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "Fechar" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "Versão Atual" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "BAIXAR" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "Recusar" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "Recusar, desinstalar o openpilot" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "Desenvolvedor" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "Dispositivo" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "Desativar ao pressionar o acelerador" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "Desativar para Desligar" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "Desativar para Reiniciar" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "Desativar para Redefinir Calibração" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "Exibir velocidade em km/h em vez de mph." + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "ID do Dongle" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "Baixar" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "Câmera do Motorista" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "Personalidade de Condução" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "ERRO" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "MODO EXPERIMENTAL ATIVO" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "Ativar" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "Ativar ADB" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "Ativar alertas de saída de faixa" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "Ativar openpilot" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "Ativar SSH" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "Ativar alertas de saída de faixa" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "" +"Ativar monitoramento do motorista mesmo quando o openpilot não está engajado." + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "Ativar openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"Ative a opção de controle longitudinal do openpilot (alpha) para permitir o " +"Modo Experimental." + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "Digite seu nome de usuário do GitHub" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "Modo Experimental" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"O Modo Experimental está indisponível neste carro pois o ACC original do " +"carro é usado para controle longitudinal." + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "" + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "Concluir Configuração" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Modo Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"Para máxima efetividade, leve seu dispositivo para dentro e conecte a um bom " +"adaptador USB-C e Wi‑Fi semanalmente.\n" +"\n" +"O Modo Firehose também pode funcionar enquanto você dirige se estiver " +"conectado a um hotspot ou a um SIM ilimitado.\n" +"\n" +"\n" +"Perguntas Frequentes\n" +"\n" +"Importa como ou onde eu dirijo? Não, apenas dirija como normalmente.\n" +"\n" +"Todos os meus segmentos são puxados no Modo Firehose? Não, puxamos " +"seletivamente um subconjunto dos seus segmentos.\n" +"\n" +"Qual é um bom adaptador USB‑C? Qualquer carregador rápido de telefone ou " +"laptop serve.\n" +"\n" +"Importa qual software eu executo? Sim, apenas o openpilot upstream (e forks " +"específicos) podem ser usados para treinamento." + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "BOM" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "Acesse https://connect.comma.ai no seu telefone" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "ALTO" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "Rede" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "INATIVO: conecte a uma rede sem franquia" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "INSTALAR" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "Instalar Atualização" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "Modo de Depuração do Joystick" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "CARREGANDO" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "Modo de Manobra Longitudinal" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "MÁX" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" +"Maximize seus envios de dados de treinamento para melhorar os modelos de " +"condução do openpilot." + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "NÃO" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "Rede" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "Nenhuma chave SSH encontrada" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "Nenhuma chave SSH encontrada para o usuário '{username}'" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "Sem notas de versão disponíveis." + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "OFFLINE" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "OK" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "ONLINE" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "Abrir" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "EMPARELHAR" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "PRÉVIA" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "RECURSOS PRIME:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "Emparelhar Dispositivo" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "Emparelhar dispositivo" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "Emparelhe seu dispositivo à sua conta comma" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"Emparelhe seu dispositivo com o comma connect (connect.comma.ai) e resgate " +"sua oferta comma prime." + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "Conecte-se ao Wi‑Fi para concluir o emparelhamento inicial" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "Desligar" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"Pré-visualize a câmera voltada para o motorista para garantir que o " +"monitoramento do motorista tenha boa visibilidade. (veículo deve estar " +"desligado)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "Erro no QR Code" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "REMOVER" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "REDEFINIR" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "REVISAR" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "Reiniciar" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "Reiniciar Dispositivo" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "Reiniciar e Atualizar" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"Receba alertas para voltar à faixa quando seu veículo cruzar uma linha de " +"faixa detectada sem seta ativada ao dirigir acima de 31 mph (50 km/h)." + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "Gravar e Enviar Câmera do Motorista" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "Gravar e Enviar Áudio do Microfone" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"Grave e armazene o áudio do microfone enquanto dirige. O áudio será incluído " +"no vídeo da dashcam no comma connect." + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "Regulatório" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "Relaxado" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "Acesso remoto" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "Capturas remotas" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "Tempo da solicitação esgotado" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "Redefinir" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "Redefinir Calibração" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "Revisar Guia de Treinamento" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "Revise as regras, recursos e limitações do openpilot" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "" + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "Selecione um idioma" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "Serial" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "Adiar Atualização" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "Software" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "Padrão" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"Padrão é recomendado. No modo agressivo, o openpilot seguirá veículos à " +"frente mais de perto e será mais agressivo com acelerador e freio. No modo " +"relaxado, o openpilot ficará mais longe dos veículos à frente. Em carros " +"compatíveis, você pode alternar essas personalidades com o botão de " +"distância do volante." + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "Sistema sem resposta" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "ASSUMA O CONTROLE IMEDIATAMENTE" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "TEMP" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "Alternâncias" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "DESINSTALAR" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "ATUALIZAR" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "Desinstalar" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "Desconhecido" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "Atualizações são baixadas apenas com o carro desligado." + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "Atualizar Agora" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"Envie dados da câmera voltada para o motorista e ajude a melhorar o " +"algoritmo de monitoramento do motorista." + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "Usar Sistema Métrico" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"Use o sistema openpilot para controle de cruzeiro adaptativo e assistência " +"de permanência em faixa. Sua atenção é necessária o tempo todo para usar " +"este recurso." + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "VEÍCULO" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "VER" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "Aguardando para iniciar" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"Aviso: Isso concede acesso SSH a todas as chaves públicas nas suas " +"configurações do GitHub. Nunca informe um nome de usuário do GitHub que não " +"seja o seu. Um funcionário da comma NUNCA pedirá para você adicionar o nome " +"de usuário do GitHub dele." + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "Bem-vindo ao openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "" +"Quando ativado, pressionar o pedal do acelerador desengajará o openpilot." + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "Você deve aceitar os Termos e Condições para usar o openpilot." + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"Você deve aceitar os Termos e Condições para usar o openpilot. Leia os " +"termos mais recentes em https://comma.ai/terms antes de continuar." + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "câmera iniciando" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "para baixo" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "falha ao verificar atualização" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "km/h" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "à esquerda" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "mph" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "nunca" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "agora" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "Controle Longitudinal do openpilot (Alpha)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot Indisponível" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"o openpilot dirige por padrão no modo chill. O Modo Experimental habilita " +"recursos em nível alpha que não estão prontos para o modo chill. Os recursos " +"experimentais são listados abaixo:

Controle Longitudinal End-to-End
Permita que o modelo de condução controle o acelerador e os freios. O " +"openpilot dirigirá como acha que um humano faria, incluindo parar em sinais " +"e semáforos vermelhos. Como o modelo decide a velocidade, a velocidade " +"definida atuará apenas como limite superior. Este é um recurso de qualidade " +"alpha; erros devem ser esperados.

Nova Visualização de Condução
A visualização de condução mudará para a câmera grande-angular " +"voltada para a estrada em baixas velocidades para mostrar melhor algumas " +"curvas. O logotipo do Modo Experimental também será exibido no canto " +"superior direito." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +" Alterar esta configuração reiniciará o openpilot se o carro estiver ligado." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"o openpilot aprende a dirigir observando humanos, como você, dirigirem.\n" +"\n" +"O Modo Firehose permite maximizar seus envios de dados de treinamento para " +"melhorar os modelos de condução do openpilot. Mais dados significam modelos " +"maiores, o que significa um Modo Experimental melhor." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "" +"o controle longitudinal do openpilot pode vir em uma atualização futura." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"o openpilot requer que o dispositivo seja montado dentro de 4° para a " +"esquerda ou direita e dentro de 5° para cima ou 9° para baixo." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "à direita" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "para cima" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "atualizado, última verificação: nunca" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "atualizado, última verificação: {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "atualização disponível" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} ALERTA" +msgstr[1] "{} ALERTAS" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{} dia atrás" +msgstr[1] "{} dias atrás" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{} hora atrás" +msgstr[1] "{} horas atrás" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{} minuto atrás" +msgstr[1] "{} minutos atrás" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "" +"{} segmento da sua condução está no conjunto de treinamento até agora." +msgstr[1] "" +"{} segmentos da sua condução estão no conjunto de treinamento até agora." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ ASSINADO" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Modo Firehose 🔥" diff --git a/selfdrive/ui/translations/app_th.po b/selfdrive/ui/translations/app_th.po new file mode 100644 index 0000000000..f2e56f2882 --- /dev/null +++ b/selfdrive/ui/translations/app_th.po @@ -0,0 +1,1129 @@ +# Thai translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-22 16:32-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: th\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

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

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "" +msgstr[1] "" + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "" diff --git a/selfdrive/ui/translations/app_tr.po b/selfdrive/ui/translations/app_tr.po new file mode 100644 index 0000000000..10191234a1 --- /dev/null +++ b/selfdrive/ui/translations/app_tr.po @@ -0,0 +1,1210 @@ +# Turkish translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-20 18:19-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " Direksiyon tork tepkisi kalibrasyonu tamamlandı." + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " Direksiyon tork tepkisi kalibrasyonu {}% tamamlandı." + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " Cihazınız {:.1f}° {} ve {:.1f}° {} yönünde konumlandırılmış." + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 yıl sürüş depolaması" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "7/24 LTE bağlantısı" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"UYARI: Bu araç için openpilot boylamsal kontrolü alfa aşamasındadır ve " +"Otomatik Acil Frenlemeyi (AEB) devre dışı bırakacaktır.

Bu araçta " +"openpilot, openpilot'un boylamsal kontrolü yerine aracın yerleşik ACC'sini " +"varsayılan olarak kullanır. openpilot boylamsal kontrolüne geçmek için bunu " +"etkinleştirin. openpilot boylamsal kontrol alfayı etkinleştirirken Deneysel " +"modu etkinleştirmeniz önerilir." + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

Steering lag calibration is {}% complete." +msgstr "" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "AKTİF" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB (Android Debug Bridge), cihazınıza USB veya ağ üzerinden bağlanmayı " +"sağlar. Daha fazla bilgi için https://docs.comma.ai/how-to/connect-to-comma " +"adresine bakın." + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "EKLE" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "Aşırı Müdahaleyi Onayla" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "Agresif" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "Kabul et" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "Sürekli Sürücü İzleme" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "" +"openpilot boylamsal kontrolünün alfa sürümü, Deneysel mod ile birlikte, " +"yayın dışı dallarda test edilebilir." + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "Kapatmak istediğinizden emin misiniz?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "Yeniden başlatmak istediğinizden emin misiniz?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "Kalibrasyonu sıfırlamak istediğinizden emin misiniz?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "Kaldırmak istediğinizden emin misiniz?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "Geri" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "connect.comma.ai adresinde comma prime üyesi olun" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "" +"connect.comma.ai'yi ana ekranınıza ekleyerek bir uygulama gibi kullanın" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "DEĞİŞTİR" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "KONTROL ET" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "CHILL MODU AÇIK" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "BAĞLAN" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "BAĞLAN" + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "Dili Değiştir" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "" +" Bu ayarı değiştirmek, araç çalışıyorsa openpilot'u yeniden başlatacaktır." + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "\"yeni cihaz ekle\"ye tıklayın ve sağdaki QR kodunu tarayın" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "Kapat" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "Geçerli Sürüm" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "İNDİR" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "Reddet" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "Reddet, openpilot'u kaldır" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "Geliştirici" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "Cihaz" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "Gaz Pedalında Devreden Çık" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "Kapatmak için Devreden Çıkın" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "Yeniden Başlatmak için Devreden Çıkın" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "Kalibrasyonu Sıfırlamak için Devreden Çıkın" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "Hızı mph yerine km/h olarak göster." + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "Dongle ID" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "İndir" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "Sürücü Kamerası" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "Sürüş Kişiliği" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "HATA" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "DENEYSEL MOD AÇIK" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "Etkinleştir" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "ADB'yi Etkinleştir" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "Şerit Terk Uyarılarını Etkinleştir" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "openpilot'u etkinleştir" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "SSH'yi Etkinleştir" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "Şerit Terk Uyarılarını Etkinleştir" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "openpilot devrede değilken bile sürücü izlemesini etkinleştir." + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "openpilot'u etkinleştir" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "" +"Deneysel modu etkinleştirmek için openpilot boylamsal kontrolünü (alfa) açın." + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "GitHub kullanıcı adınızı girin" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "Deneysel Mod" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "" +"Bu araçta boylamsal kontrol için stok ACC kullanıldığından şu anda Deneysel " +"mod kullanılamıyor." + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "" + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "Kurulumu Bitir" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Firehose Modu" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"Maksimum verim için cihazınızı içeri alın ve haftalık olarak iyi bir USB-C " +"adaptörüne ve Wi‑Fi'a bağlayın.\n" +"\n" +"Firehose Modu, bir hotspot'a veya sınırsız SIM karta bağlıyken sürüş " +"sırasında da çalışabilir.\n" +"\n" +"\n" +"Sıkça Sorulan Sorular\n" +"\n" +"Nasıl veya nerede sürdüğüm önemli mi? Hayır, normalde nasıl sürüyorsanız " +"öyle sürün.\n" +"\n" +"Firehose Modu'nda tüm segmentlerim çekiliyor mu? Hayır, segmentlerinizin bir " +"alt kümesini seçerek çekiyoruz.\n" +"\n" +"İyi bir USB‑C adaptörü nedir? Hızlı bir telefon veya dizüstü şarj cihazı " +"uygundur.\n" +"\n" +"Hangi yazılımı çalıştırdığım önemli mi? Evet, yalnızca upstream openpilot " +"(ve bazı fork'lar) eğitim için kullanılabilir." + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "İYİ" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "Telefonunuzda https://connect.comma.ai adresine gidin" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "YÜKSEK" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "Ağ" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "PASİF: sınırsız bir ağa bağlanın" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "YÜKLE" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "Güncellemeyi Yükle" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "Joystick Hata Ayıklama Modu" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "YÜKLENİYOR" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "Boylamsal Manevra Modu" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "MAKS" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "" +"openpilot'un sürüş modellerini iyileştirmek için eğitim veri yüklemelerinizi " +"en üst düzeye çıkarın." + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "HAYIR" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "Ağ" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "SSH anahtarı bulunamadı" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "'{username}' için SSH anahtarı bulunamadı" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "Sürüm notu mevcut değil." + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "ÇEVRİMDIŞI" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "OK" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "ÇEVRİMİÇİ" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "Aç" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "EŞLE" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "ÖNİZLEME" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "PRIME ÖZELLİKLERİ:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "Cihazı Eşle" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "Cihazı eşle" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "Cihazınızı comma hesabınızla eşleştirin" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"Cihazınızı comma connect (connect.comma.ai) ile eşleştirin ve comma prime " +"teklifinizi alın." + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "İlk eşleştirmeyi tamamlamak için lütfen Wi‑Fi'a bağlanın" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "Kapat" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "" +"Sürücü izleme görünürlüğünün iyi olduğundan emin olmak için sürücüye bakan " +"kamerayı önizleyin. (araç kapalı olmalıdır)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "QR Kod Hatası" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "KALDIR" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "SIFIRLA" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "GÖZDEN GEÇİR" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "Yeniden Başlat" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "Cihazı Yeniden Başlat" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "Yeniden Başlat ve Güncelle" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"Araç 31 mph (50 km/h) üzerindeyken sinyal verilmeden algılanan şerit " +"çizgisini aştığınızda şeride geri dönmeniz için uyarılar alın." + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "Sürücü Kamerasını Kaydet ve Yükle" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "Mikrofon Sesini Kaydet ve Yükle" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"Sürüş sırasında mikrofon sesini kaydedip saklayın. Ses, comma connect'teki " +"ön kamera videosuna dahil edilecektir." + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "Mevzuat" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "Rahat" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "Uzaktan erişim" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "Uzaktan anlık görüntüler" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "İstek zaman aşımına uğradı" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "Sıfırla" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "Kalibrasyonu Sıfırla" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "Eğitim Kılavuzunu İncele" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "" +"openpilot'un kurallarını, özelliklerini ve sınırlamalarını gözden geçirin" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "" + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "Bir dil seçin" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "Seri" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "Güncellemeyi Ertele" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "Yazılım" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "Standart" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"Standart önerilir. Agresif modda openpilot öndeki aracı daha yakından takip " +"eder ve gaz/fren kullanımında daha ataktır. Rahat modda openpilot öndeki " +"araçlardan daha uzak durur. Desteklenen araçlarda bu kişilikler arasında " +"direksiyon mesafe düğmesiyle geçiş yapabilirsiniz." + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "Sistem Yanıt Vermiyor" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "HEMEN KONTROLÜ DEVRALIN" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "TEMP" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "Seçenekler" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "KALDIR" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "GÜNCELLE" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "Kaldır" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "Bilinmiyor" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "Güncellemeler yalnızca araç kapalıyken indirilir." + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "Şimdi Yükselt" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "" +"Sürücüye bakan kameradan veri yükleyin ve sürücü izleme algoritmasını " +"geliştirmeye yardımcı olun." + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "Metrik Sistemi Kullan" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"Uyarlanabilir hız sabitleyici ve şerit koruma sürücü yardımında openpilot " +"sistemini kullanın. Bu özelliği kullanırken her zaman dikkatli olmanız " +"gerekir." + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "ARAÇ" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "GÖRÜNTÜLE" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "Başlatma bekleniyor" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"Uyarı: Bu, GitHub ayarlarınızdaki tüm açık anahtarlara SSH erişimi verir. " +"Kendi adınız dışında asla bir GitHub kullanıcı adı girmeyin. Bir comma " +"çalışanı sizden asla GitHub kullanıcı adlarını eklemenizi İSTEMEZ." + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "openpilot'a hoş geldiniz" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "" +"Etkinleştirildiğinde, gaz pedalına basmak openpilot'u devreden çıkarır." + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "openpilot'u kullanmak için Şartlar ve Koşulları kabul etmelisiniz." + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"openpilot'u kullanmak için Şartlar ve Koşulları kabul etmelisiniz. Devam " +"etmeden önce en güncel şartları https://comma.ai/terms adresinde okuyun." + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "kamera başlatılıyor" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "aşağı" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "güncelleme kontrolü başarısız" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "km/h" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "sol" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "mph" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "asla" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "şimdi" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "openpilot Boylamsal Kontrol (Alfa)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot Kullanılamıyor" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot varsayılan olarak chill modunda sürer. Deneysel mod, chill moduna " +"hazır olmayan alfa seviyesindeki özellikleri etkinleştirir. Deneysel " +"özellikler aşağıda listelenmiştir:

Uçtan Uca Boylamsal Kontrol
Sürüş modelinin gaz ve frenleri kontrol etmesine izin verin. " +"openpilot, kırmızı ışıklarda ve dur işaretlerinde durmak dahil, bir insan " +"nasıl sürer diye düşündüğüne göre sürer. Hızı sürüş modeli belirlediğinden, " +"ayarlanan hız yalnızca üst sınır olarak işlev görür. Bu bir alfa kalitesinde " +"özelliktir; hatalar beklenmelidir.

Yeni Sürüş Görselleştirmesi
Sürüş görselleştirmesi, düşük hızlarda bazı dönüşleri daha iyi " +"göstermek için yola bakan geniş açılı kameraya geçer. Deneysel mod logosu " +"sağ üst köşede de gösterilecektir." + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +" Bu ayarı değiştirmek, araç çalışıyorsa openpilot'u yeniden başlatacaktır." + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot, sizin gibi insanların nasıl sürdüğünü izleyerek sürmeyi öğrenir.\n" +"\n" +"Firehose Modu, openpilot'un sürüş modellerini geliştirmek için eğitim veri " +"yüklemelerinizi en üst düzeye çıkarmanıza olanak tanır. Daha fazla veri, " +"daha büyük modeller demektir; bu da daha iyi Deneysel Mod anlamına gelir." + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "openpilot boylamsal kontrolü gelecekteki bir güncellemede gelebilir." + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "" +"openpilot, cihazın sağa/sola 4° ve yukarı 5° veya aşağı 9° içinde monte " +"edilmesini gerektirir." + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "sağ" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "yukarı" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "güncel, son kontrol asla" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "güncel, son kontrol {}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "güncelleme mevcut" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} UYARI" +msgstr[1] "{} UYARILAR" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{} gün önce" +msgstr[1] "{} gün önce" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{} saat önce" +msgstr[1] "{} saat önce" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{} dakika önce" +msgstr[1] "{} dakika önce" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "{} segment sürüşünüz eğitim veri setinde." +msgstr[1] "{} segment sürüşünüz eğitim veri setinde." + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ ABONE" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Firehose Modu 🔥" diff --git a/selfdrive/ui/translations/app_zh-CHS.po b/selfdrive/ui/translations/app_zh-CHS.po new file mode 100644 index 0000000000..16e4369476 --- /dev/null +++ b/selfdrive/ui/translations/app_zh-CHS.po @@ -0,0 +1,1174 @@ +# Language zh-CHS translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-22 16:32-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh-CHS\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " 转向扭矩响应校准完成。" + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " 转向扭矩响应校准已完成 {}%。" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " 您的设备朝向 {:.1f}° {} 与 {:.1f}° {}。" + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 年行驶数据存储" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "全天候 LTE 连接" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"警告:此车型的 openpilot 纵向控制仍为 alpha,将会停用自动紧急制动 (AEB)。" +"

在此车型上,openpilot 默认使用车载 ACC,而非 openpilot 的纵向控" +"制。启用此选项可切换为 openpilot 纵向控制。建议同时启用实验模式。若车辆通电," +"更改此设置将会重启 openpilot。" + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "

转向延迟校准完成。" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

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

转向延迟校准已完成 {}%。" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "已启用" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB(Android 调试桥)可通过 USB 或网络连接到您的设备。详见 https://docs." +"comma.ai/how-to/connect-to-comma。" + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "添加" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "APN 设置" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "确认过度作动" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "高级" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "激进" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "同意" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "始终启用驾驶员监控" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "openpilot 纵向控制的 alpha 版本可在非发布分支搭配实验模式进行测试。" + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "确定要关机吗?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "确定要重启吗?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "确定要重置校准吗?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "确定要卸载吗?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "返回" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "前往 connect.comma.ai 成为 comma prime 会员" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "将 connect.comma.ai 添加到主屏幕,像应用一样使用" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "更改" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "检查" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "安稳模式已开启" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "CONNECT" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "CONNECTING..." + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "取消" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "蜂窝计量" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "更改语言" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "若车辆通电,更改此设置将重启 openpilot。" + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "点击“添加新设备”,扫描右侧二维码" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "关闭" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "当前版本" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "下载" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "拒绝" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "拒绝并卸载 openpilot" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "开发者" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "设备" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "踩下加速踏板时脱离" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "脱离以关机" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "脱离以重启" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "脱离以重置校准" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "以 km/h 显示速度(非 mph)。" + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "Dongle ID" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "下载" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "车内摄像头" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "驾驶风格" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "编辑" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "错误" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "实验模式已开启" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "启用" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "启用 ADB" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "启用车道偏离警示" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "启用漫游" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "启用 SSH" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "启用网络共享" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "即使未启用 openpilot 也启用驾驶员监控。" + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "启用 openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "启用 openpilot 纵向控制(alpha)开关,以使用实验模式。" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "输入 APN" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "输入 SSID" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "输入新的网络共享密码" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "输入密码" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "输入您的 GitHub 用户名" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "错误" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "实验模式" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "此车型当前无法使用实验模式,因为纵向控制使用的是原厂 ACC。" + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "正在遗忘..." + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "完成设置" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Firehose 模式" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"为达到最佳效果,请将设备带到室内,并每周连接优质 USB‑C 充电器与 Wi‑Fi。\n" +"\n" +"若连接热点或不限流量卡,行车中也可使用 Firehose 模式。\n" +"\n" +"\n" +"常见问题\n" +"\n" +"我怎么开、在哪开有区别吗?没有,平常怎么开就怎么开。\n" +"\n" +"Firehose 模式会拉取我所有片段吗?不会,我们会选择性拉取部分片段。\n" +"\n" +"什么是好的 USB‑C 充电器?任何快速的手机或笔电充电器都可以。\n" +"\n" +"我跑什么软件有区别吗?有,只有上游 openpilot(及特定分支)可用于训练。" + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "忘记" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "要忘记 Wi‑Fi 网络“{}”吗?" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "良好" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "在手机上前往 https://connect.comma.ai" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "高" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "隐藏网络" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "未启用:请连接不限流量网络" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "安装" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "IP 地址" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "安装更新" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "摇杆调试模式" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "加载中" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "纵向操作模式" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "最大" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "最大化上传训练数据,以改进 openpilot 的驾驶模型。" + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "无" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "否" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "网络" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "未找到 SSH 密钥" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "未找到用户“{}”的 SSH 密钥" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "暂无发行说明。" + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "离线" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "确定" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "在线" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "打开" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "配对" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "预览" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "PRIME 功能:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "配对设备" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "配对设备" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "将设备配对到您的 comma 账号" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"将设备与 comma connect(connect.comma.ai)配对,领取您的 comma prime 优惠。" + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "请连接 Wi‑Fi 以完成初始配对" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "关机" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "在计量制 Wi‑Fi 连接时避免大量上传" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "在计量制蜂窝网络时避免大量上传" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "预览车内摄像头以确保驾驶员监控视野良好。(车辆必须熄火)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "二维码错误" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "移除" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "重置" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "查看" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "重启" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "重启设备" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "重启并更新" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"当车辆以超过 31 mph(50 km/h)行驶且未打转向灯越过检测到的车道线时,接收引导" +"回车道的警报。" + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "录制并上传车内摄像头" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "录制并上传麦克风音频" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"行驶时录制并保存麦克风音频。音频将包含在 comma connect 的行车记录视频中。" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "法规" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "从容" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "远程访问" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "远程快照" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "请求超时" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "重置" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "重置校准" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "查看训练指南" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "查看 openpilot 的规则、功能与限制" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "选择" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "SSH 密钥" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "正在扫描 Wi‑Fi 网络…" + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "选择" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "选择分支" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "选择语言" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "序列号" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "延后更新" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "软件" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "标准" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"建议使用标准模式。激进模式下,openpilot 会更贴近前车,油门与刹车更为激进;从" +"容模式下,会与前车保持更远距离。在支持的车型上,可用方向盘距离按钮切换这些风" +"格。" + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "系统无响应" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "请立即接管控制" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "温度" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "目标分支" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "网络共享密码" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "切换" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "卸载" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "更新" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "卸载" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "未知" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "仅在车辆熄火时下载更新。" + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "立即升级" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "上传车内摄像头数据,帮助改进驾驶员监控算法。" + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "使用公制" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"使用 openpilot 进行自适应巡航与车道保持辅助。使用此功能时,您必须始终保持专" +"注。" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "车辆" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "查看" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "等待开始" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"警告:这将授予对您 GitHub 设置中所有公钥的 SSH 访问权限。请勿输入非您本人的 " +"GitHub 用户名。comma 员工绝不会要求您添加他们的用户名。" + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "欢迎使用 openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "启用后,踩下加速踏板将会脱离 openpilot。" + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "Wi‑Fi 计量网络" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "密码错误" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "您必须接受条款与条件才能使用 openpilot。" + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"您必须接受条款与条件才能使用 openpilot。继续前请阅读 https://comma.ai/terms " +"上的最新条款。" + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "相机启动中" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "默认" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "下" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "检查更新失败" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "用于“{}”" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "公里/时" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "留空以自动配置" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "左" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "计量" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "英里/时" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "从不" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "现在" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "openpilot 纵向控制(Alpha)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot 无法使用" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot 默认以安稳模式行驶。实验模式会启用尚未准备好用于安稳模式的 Alpha 级" +"功能。实验功能如下:

端到端纵向控制


让驾驶模型控制油门与刹车。" +"openpilot 会像人类一样驾驶,包括在红灯与停牌前停车。由于驾驶模型决定行驶速" +"度,设定速度仅作为上限。这是 Alpha 质量功能;预期会有错误。

全新驾驶可" +"视化


在低速时,驾驶可视化将切换至面向道路的广角摄像头以更好显示部分转" +"弯。右上角也会显示实验模式图标。" + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +"openpilot 持续进行校准,通常无需重置。若车辆通电,重置校准将会重启 " +"openpilot。" + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot 通过观察人类(例如您)的驾驶来学习。\n" +"\n" +"Firehose 模式可让您最大化上传训练数据,以改进 openpilot 的驾驶模型。更多数据" +"意味着更大的模型,也意味着更好的实验模式。" + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "openpilot 纵向控制可能会在未来更新中提供。" + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "openpilot 要求设备安装在左右 4°、上 5° 或下 9° 以内。" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "右" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "不限流量" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "上" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "已是最新,最后检查:从未" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "已是最新,最后检查:{}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "有可用更新" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} 条警报" +msgstr[1] "{} 条警报" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{} 天前" +msgstr[1] "{} 天前" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{} 小时前" +msgstr[1] "{} 小时前" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{} 分钟前" +msgstr[1] "{} 分钟前" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "目前已有 {} 个您的驾驶片段被纳入训练数据集。" +msgstr[1] "目前已有 {} 个您的驾驶片段被纳入训练数据集。" + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ 已订阅" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Firehose 模式 🔥" diff --git a/selfdrive/ui/translations/app_zh-CHT.po b/selfdrive/ui/translations/app_zh-CHT.po new file mode 100644 index 0000000000..85cfb77401 --- /dev/null +++ b/selfdrive/ui/translations/app_zh-CHT.po @@ -0,0 +1,1173 @@ +# Language zh-CHT translations for PACKAGE package. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Automatically generated, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-23 00:50-0700\n" +"PO-Revision-Date: 2025-10-22 16:32-0700\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh-CHT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: selfdrive/ui/layouts/settings/device.py:160 +#, python-format +msgid " Steering torque response calibration is complete." +msgstr " 轉向扭矩回應校正完成。" + +#: selfdrive/ui/layouts/settings/device.py:158 +#, python-format +msgid " Steering torque response calibration is {}% complete." +msgstr " 轉向扭矩回應校正已完成 {}%。" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." +msgstr " 您的裝置朝向 {:.1f}° {} 與 {:.1f}° {}。" + +#: selfdrive/ui/layouts/sidebar.py:43 +msgid "--" +msgstr "--" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "1 year of drive storage" +msgstr "1 年行駛資料儲存" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "24/7 LTE connectivity" +msgstr "全年無休 LTE 連線" + +#: selfdrive/ui/layouts/sidebar.py:46 +msgid "2G" +msgstr "2G" + +#: selfdrive/ui/layouts/sidebar.py:47 +msgid "3G" +msgstr "3G" + +#: selfdrive/ui/layouts/sidebar.py:49 +msgid "5G" +msgstr "5G" + +#: selfdrive/ui/layouts/settings/developer.py:23 +msgid "" +"WARNING: openpilot longitudinal control is in alpha for this car and will " +"disable Automatic Emergency Braking (AEB).

On this car, openpilot " +"defaults to the car's built-in ACC instead of openpilot's longitudinal " +"control. Enable this to switch to openpilot longitudinal control. Enabling " +"Experimental mode is recommended when enabling openpilot longitudinal " +"control alpha. Changing this setting will restart openpilot if the car is " +"powered on." +msgstr "" +"警告:此車款的 openpilot 縱向控制仍為 alpha,將會停用自動緊急煞車 (AEB)。" +"

在此車款上,openpilot 預設使用車載 ACC,而非 openpilot 的縱向控" +"制。啟用此選項可切換為 openpilot 縱向控制。建議同時啟用實驗模式。若車輛通電," +"變更此設定將會重新啟動 openpilot。" + +#: selfdrive/ui/layouts/settings/device.py:148 +#, python-format +msgid "

Steering lag calibration is complete." +msgstr "

轉向延遲校正完成。" + +#: selfdrive/ui/layouts/settings/device.py:146 +#, python-format +msgid "

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

轉向延遲校正已完成 {}%。" + +#: selfdrive/ui/layouts/settings/firehose.py:138 +#, python-format +msgid "ACTIVE" +msgstr "啟用" + +#: selfdrive/ui/layouts/settings/developer.py:15 +msgid "" +"ADB (Android Debug Bridge) allows connecting to your device over USB or over " +"the network. See https://docs.comma.ai/how-to/connect-to-comma for more info." +msgstr "" +"ADB (Android Debug Bridge) 可透過 USB 或網路連線至您的裝置。詳見 https://" +"docs.comma.ai/how-to/connect-to-comma。" + +#: selfdrive/ui/widgets/ssh_key.py:30 +msgid "ADD" +msgstr "新增" + +#: system/ui/widgets/network.py:139 +#, python-format +msgid "APN Setting" +msgstr "APN 設定" + +#: selfdrive/ui/widgets/offroad_alerts.py:109 +#, python-format +msgid "Acknowledge Excessive Actuation" +msgstr "確認過度作動" + +#: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 +#, python-format +msgid "Advanced" +msgstr "進階" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Aggressive" +msgstr "積極" + +#: selfdrive/ui/layouts/onboarding.py:116 +#, python-format +msgid "Agree" +msgstr "同意" + +#: selfdrive/ui/layouts/settings/toggles.py:70 +#, python-format +msgid "Always-On Driver Monitoring" +msgstr "持續啟用駕駛監控" + +#: selfdrive/ui/layouts/settings/toggles.py:186 +#, python-format +msgid "" +"An alpha version of openpilot longitudinal control can be tested, along with " +"Experimental mode, on non-release branches." +msgstr "openpilot 縱向控制的 alpha 版本可於非發行分支搭配實驗模式進行測試。" + +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Are you sure you want to power off?" +msgstr "確定要關機嗎?" + +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Are you sure you want to reboot?" +msgstr "確定要重新啟動嗎?" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Are you sure you want to reset calibration?" +msgstr "確定要重設校正嗎?" + +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Are you sure you want to uninstall?" +msgstr "確定要解除安裝嗎?" + +#: system/ui/widgets/network.py:99 selfdrive/ui/layouts/onboarding.py:147 +#, python-format +msgid "Back" +msgstr "返回" + +#: selfdrive/ui/widgets/prime.py:38 +#, python-format +msgid "Become a comma prime member at connect.comma.ai" +msgstr "前往 connect.comma.ai 成為 comma prime 會員" + +#: selfdrive/ui/widgets/pairing_dialog.py:130 +#, python-format +msgid "Bookmark connect.comma.ai to your home screen to use it like an app" +msgstr "將 connect.comma.ai 加到主畫面,像 App 一樣使用" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "CHANGE" +msgstr "變更" + +#: selfdrive/ui/layouts/settings/software.py:50 +#: selfdrive/ui/layouts/settings/software.py:107 +#: selfdrive/ui/layouts/settings/software.py:118 +#: selfdrive/ui/layouts/settings/software.py:147 +#, python-format +msgid "CHECK" +msgstr "檢查" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "CHILL MODE ON" +msgstr "安穩模式已開啟" + +#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73 +#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:138 +#, python-format +msgid "CONNECT" +msgstr "CONNECT" + +#: system/ui/widgets/network.py:369 +#, python-format +msgid "CONNECTING..." +msgstr "CONNECTING..." + +#: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 +#: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 +#, python-format +msgid "Cancel" +msgstr "取消" + +#: system/ui/widgets/network.py:134 +#, python-format +msgid "Cellular Metered" +msgstr "行動網路計量" + +#: selfdrive/ui/layouts/settings/device.py:68 +#, python-format +msgid "Change Language" +msgstr "變更語言" + +#: selfdrive/ui/layouts/settings/toggles.py:125 +#, python-format +msgid "Changing this setting will restart openpilot if the car is powered on." +msgstr "若車輛通電,變更此設定將重新啟動 openpilot。" + +#: selfdrive/ui/widgets/pairing_dialog.py:129 +#, python-format +msgid "Click \"add new device\" and scan the QR code on the right" +msgstr "點選「新增裝置」,掃描右側 QR 碼" + +#: selfdrive/ui/widgets/offroad_alerts.py:104 +#, python-format +msgid "Close" +msgstr "關閉" + +#: selfdrive/ui/layouts/settings/software.py:49 +#, python-format +msgid "Current Version" +msgstr "目前版本" + +#: selfdrive/ui/layouts/settings/software.py:110 +#, python-format +msgid "DOWNLOAD" +msgstr "下載" + +#: selfdrive/ui/layouts/onboarding.py:115 +#, python-format +msgid "Decline" +msgstr "拒絕" + +#: selfdrive/ui/layouts/onboarding.py:148 +#, python-format +msgid "Decline, uninstall openpilot" +msgstr "拒絕並解除安裝 openpilot" + +#: selfdrive/ui/layouts/settings/settings.py:67 +msgid "Developer" +msgstr "開發人員" + +#: selfdrive/ui/layouts/settings/settings.py:62 +msgid "Device" +msgstr "裝置" + +#: selfdrive/ui/layouts/settings/toggles.py:58 +#, python-format +msgid "Disengage on Accelerator Pedal" +msgstr "踩下加速踏板時脫離" + +#: selfdrive/ui/layouts/settings/device.py:184 +#, python-format +msgid "Disengage to Power Off" +msgstr "脫離以關機" + +#: selfdrive/ui/layouts/settings/device.py:172 +#, python-format +msgid "Disengage to Reboot" +msgstr "脫離以重新啟動" + +#: selfdrive/ui/layouts/settings/device.py:103 +#, python-format +msgid "Disengage to Reset Calibration" +msgstr "脫離以重設校正" + +#: selfdrive/ui/layouts/settings/toggles.py:32 +msgid "Display speed in km/h instead of mph." +msgstr "以 km/h 顯示速度(非 mph)。" + +#: selfdrive/ui/layouts/settings/device.py:59 +#, python-format +msgid "Dongle ID" +msgstr "Dongle ID" + +#: selfdrive/ui/layouts/settings/software.py:50 +#, python-format +msgid "Download" +msgstr "下載" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "Driver Camera" +msgstr "車內鏡頭" + +#: selfdrive/ui/layouts/settings/toggles.py:96 +#, python-format +msgid "Driving Personality" +msgstr "駕駛風格" + +#: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 +#, python-format +msgid "EDIT" +msgstr "編輯" + +#: selfdrive/ui/layouts/sidebar.py:138 +msgid "ERROR" +msgstr "錯誤" + +#: selfdrive/ui/layouts/sidebar.py:45 +msgid "ETH" +msgstr "ETH" + +#: selfdrive/ui/widgets/exp_mode_button.py:50 +#, python-format +msgid "EXPERIMENTAL MODE ON" +msgstr "實驗模式已開啟" + +#: selfdrive/ui/layouts/settings/developer.py:166 +#: selfdrive/ui/layouts/settings/toggles.py:228 +#, python-format +msgid "Enable" +msgstr "啟用" + +#: selfdrive/ui/layouts/settings/developer.py:39 +#, python-format +msgid "Enable ADB" +msgstr "啟用 ADB" + +#: selfdrive/ui/layouts/settings/toggles.py:64 +#, python-format +msgid "Enable Lane Departure Warnings" +msgstr "啟用偏離車道警示" + +#: system/ui/widgets/network.py:129 +#, python-format +msgid "Enable Roaming" +msgstr "啟用漫遊" + +#: selfdrive/ui/layouts/settings/developer.py:48 +#, python-format +msgid "Enable SSH" +msgstr "啟用 SSH" + +#: system/ui/widgets/network.py:120 +#, python-format +msgid "Enable Tethering" +msgstr "啟用網路共享" + +#: selfdrive/ui/layouts/settings/toggles.py:30 +msgid "Enable driver monitoring even when openpilot is not engaged." +msgstr "即使未啟動 openpilot 亦啟用駕駛監控。" + +#: selfdrive/ui/layouts/settings/toggles.py:46 +#, python-format +msgid "Enable openpilot" +msgstr "啟用 openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:189 +#, python-format +msgid "" +"Enable the openpilot longitudinal control (alpha) toggle to allow " +"Experimental mode." +msgstr "啟用 openpilot 縱向控制(alpha)切換,以使用實驗模式。" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "Enter APN" +msgstr "輸入 APN" + +#: system/ui/widgets/network.py:241 +#, python-format +msgid "Enter SSID" +msgstr "輸入 SSID" + +#: system/ui/widgets/network.py:254 +#, python-format +msgid "Enter new tethering password" +msgstr "輸入新的網路共享密碼" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "Enter password" +msgstr "輸入密碼" + +#: selfdrive/ui/widgets/ssh_key.py:89 +#, python-format +msgid "Enter your GitHub username" +msgstr "輸入您的 GitHub 使用者名稱" + +#: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 +#, python-format +msgid "Error" +msgstr "錯誤" + +#: selfdrive/ui/layouts/settings/toggles.py:52 +#, python-format +msgid "Experimental Mode" +msgstr "實驗模式" + +#: selfdrive/ui/layouts/settings/toggles.py:181 +#, python-format +msgid "" +"Experimental mode is currently unavailable on this car since the car's stock " +"ACC is used for longitudinal control." +msgstr "此車款目前無法使用實驗模式,因為縱向控制使用的是原廠 ACC。" + +#: system/ui/widgets/network.py:373 +#, python-format +msgid "FORGETTING..." +msgstr "正在遺忘..." + +#: selfdrive/ui/widgets/setup.py:44 +#, python-format +msgid "Finish Setup" +msgstr "完成設定" + +#: selfdrive/ui/layouts/settings/settings.py:66 +msgid "Firehose" +msgstr "Firehose" + +#: selfdrive/ui/layouts/settings/firehose.py:18 +msgid "Firehose Mode" +msgstr "Firehose 模式" + +#: selfdrive/ui/layouts/settings/firehose.py:25 +msgid "" +"For maximum effectiveness, bring your device inside and connect to a good " +"USB-C adapter and Wi-Fi weekly.\n" +"\n" +"Firehose Mode can also work while you're driving if connected to a hotspot " +"or unlimited SIM card.\n" +"\n" +"\n" +"Frequently Asked Questions\n" +"\n" +"Does it matter how or where I drive? Nope, just drive as you normally " +"would.\n" +"\n" +"Do all of my segments get pulled in Firehose Mode? No, we selectively pull a " +"subset of your segments.\n" +"\n" +"What's a good USB-C adapter? Any fast phone or laptop charger should be " +"fine.\n" +"\n" +"Does it matter which software I run? Yes, only upstream openpilot (and " +"particular forks) are able to be used for training." +msgstr "" +"為達最佳效果,請將裝置帶到室內,並每週連接優質 USB‑C 充電器與 Wi‑Fi。\n" +"\n" +"若連上熱點或吃到飽門號,行車中也可使用 Firehose 模式。\n" +"\n" +"\n" +"常見問題\n" +"\n" +"我怎麼開、在哪裡開有差嗎?沒有,平常怎麼開就怎麼開。\n" +"\n" +"Firehose 模式會拉取我所有片段嗎?不會,我們會選擇性拉取部分片段。\n" +"\n" +"什麼是好的 USB‑C 充電器?任何快速的手機或筆電充電器都可以。\n" +"\n" +"我跑什麼軟體有差嗎?有,只有上游 openpilot(及特定分支)可用於訓練。" + +#: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 +#, python-format +msgid "Forget" +msgstr "忘記" + +#: system/ui/widgets/network.py:319 +#, python-format +msgid "Forget Wi-Fi Network \"{}\"?" +msgstr "要忘記 Wi‑Fi 網路「{}」嗎?" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +msgid "GOOD" +msgstr "良好" + +#: selfdrive/ui/widgets/pairing_dialog.py:128 +#, python-format +msgid "Go to https://connect.comma.ai on your phone" +msgstr "在手機上前往 https://connect.comma.ai" + +#: selfdrive/ui/layouts/sidebar.py:129 +msgid "HIGH" +msgstr "高" + +#: system/ui/widgets/network.py:155 +#, python-format +msgid "Hidden Network" +msgstr "隱藏網路" + +#: selfdrive/ui/layouts/settings/firehose.py:140 +#, python-format +msgid "INACTIVE: connect to an unmetered network" +msgstr "未啟用:請連接不限流量網路" + +#: selfdrive/ui/layouts/settings/software.py:53 +#: selfdrive/ui/layouts/settings/software.py:136 +#, python-format +msgid "INSTALL" +msgstr "安裝" + +#: system/ui/widgets/network.py:150 +#, python-format +msgid "IP Address" +msgstr "IP 位址" + +#: selfdrive/ui/layouts/settings/software.py:53 +#, python-format +msgid "Install Update" +msgstr "安裝更新" + +#: selfdrive/ui/layouts/settings/developer.py:56 +#, python-format +msgid "Joystick Debug Mode" +msgstr "搖桿除錯模式" + +#: selfdrive/ui/widgets/ssh_key.py:29 +msgid "LOADING" +msgstr "載入中" + +#: selfdrive/ui/layouts/sidebar.py:48 +msgid "LTE" +msgstr "LTE" + +#: selfdrive/ui/layouts/settings/developer.py:64 +#, python-format +msgid "Longitudinal Maneuver Mode" +msgstr "縱向操作模式" + +#: selfdrive/ui/onroad/hud_renderer.py:148 +#, python-format +msgid "MAX" +msgstr "最大" + +#: selfdrive/ui/widgets/setup.py:75 +#, python-format +msgid "" +"Maximize your training data uploads to improve openpilot's driving models." +msgstr "最大化上傳訓練資料,以改進 openpilot 的駕駛模型。" + +#: selfdrive/ui/layouts/settings/device.py:59 +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "N/A" +msgstr "無" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "NO" +msgstr "否" + +#: selfdrive/ui/layouts/settings/settings.py:63 +msgid "Network" +msgstr "網路" + +#: selfdrive/ui/widgets/ssh_key.py:114 +#, python-format +msgid "No SSH keys found" +msgstr "找不到 SSH 金鑰" + +#: selfdrive/ui/widgets/ssh_key.py:126 +#, python-format +msgid "No SSH keys found for user '{}'" +msgstr "找不到使用者 '{}' 的 SSH 金鑰" + +#: selfdrive/ui/widgets/offroad_alerts.py:320 +#, python-format +msgid "No release notes available." +msgstr "無可用發行說明。" + +#: selfdrive/ui/layouts/sidebar.py:73 selfdrive/ui/layouts/sidebar.py:134 +msgid "OFFLINE" +msgstr "離線" + +#: system/ui/widgets/html_render.py:263 system/ui/widgets/confirm_dialog.py:93 +#: selfdrive/ui/layouts/sidebar.py:127 +#, python-format +msgid "OK" +msgstr "確定" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:136 +#: selfdrive/ui/layouts/sidebar.py:144 +msgid "ONLINE" +msgstr "線上" + +#: selfdrive/ui/widgets/setup.py:20 +#, python-format +msgid "Open" +msgstr "開啟" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "PAIR" +msgstr "配對" + +#: selfdrive/ui/layouts/sidebar.py:142 +msgid "PANDA" +msgstr "PANDA" + +#: selfdrive/ui/layouts/settings/device.py:62 +#, python-format +msgid "PREVIEW" +msgstr "預覽" + +#: selfdrive/ui/widgets/prime.py:44 +#, python-format +msgid "PRIME FEATURES:" +msgstr "PRIME 功能:" + +#: selfdrive/ui/layouts/settings/device.py:48 +#, python-format +msgid "Pair Device" +msgstr "配對裝置" + +#: selfdrive/ui/widgets/setup.py:19 +#, python-format +msgid "Pair device" +msgstr "配對裝置" + +#: selfdrive/ui/widgets/pairing_dialog.py:103 +#, python-format +msgid "Pair your device to your comma account" +msgstr "將裝置配對至您的 comma 帳號" + +#: selfdrive/ui/widgets/setup.py:48 selfdrive/ui/layouts/settings/device.py:24 +#, python-format +msgid "" +"Pair your device with comma connect (connect.comma.ai) and claim your comma " +"prime offer." +msgstr "" +"將裝置與 comma connect(connect.comma.ai)配對,領取您的 comma prime 優惠。" + +#: selfdrive/ui/widgets/setup.py:91 +#, python-format +msgid "Please connect to Wi-Fi to complete initial pairing" +msgstr "請連線至 Wi‑Fi 以完成初始化配對" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:187 +#, python-format +msgid "Power Off" +msgstr "關機" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Prevent large data uploads when on a metered Wi-Fi connection" +msgstr "在計量制 Wi‑Fi 連線時避免大量上傳" + +#: system/ui/widgets/network.py:135 +#, python-format +msgid "Prevent large data uploads when on a metered cellular connection" +msgstr "在計量制行動網路時避免大量上傳" + +#: selfdrive/ui/layouts/settings/device.py:25 +msgid "" +"Preview the driver facing camera to ensure that driver monitoring has good " +"visibility. (vehicle must be off)" +msgstr "預覽車內鏡頭以確保駕駛監控視野良好。(車輛須熄火)" + +#: selfdrive/ui/widgets/pairing_dialog.py:161 +#, python-format +msgid "QR Code Error" +msgstr "QR 碼錯誤" + +#: selfdrive/ui/widgets/ssh_key.py:31 +msgid "REMOVE" +msgstr "移除" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "RESET" +msgstr "重設" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "REVIEW" +msgstr "檢視" + +#: selfdrive/ui/layouts/settings/device.py:55 +#: selfdrive/ui/layouts/settings/device.py:175 +#, python-format +msgid "Reboot" +msgstr "重新啟動" + +#: selfdrive/ui/onroad/alert_renderer.py:66 +#, python-format +msgid "Reboot Device" +msgstr "重新啟動裝置" + +#: selfdrive/ui/widgets/offroad_alerts.py:112 +#, python-format +msgid "Reboot and Update" +msgstr "重新啟動並更新" + +#: selfdrive/ui/layouts/settings/toggles.py:27 +msgid "" +"Receive alerts to steer back into the lane when your vehicle drifts over a " +"detected lane line without a turn signal activated while driving over 31 mph " +"(50 km/h)." +msgstr "" +"當車輛以超過 31 mph(50 km/h)行駛且未打方向燈越過偵測到的車道線時,接收轉向" +"回車道的警示。" + +#: selfdrive/ui/layouts/settings/toggles.py:76 +#, python-format +msgid "Record and Upload Driver Camera" +msgstr "錄製並上傳車內鏡頭" + +#: selfdrive/ui/layouts/settings/toggles.py:82 +#, python-format +msgid "Record and Upload Microphone Audio" +msgstr "錄製並上傳麥克風音訊" + +#: selfdrive/ui/layouts/settings/toggles.py:33 +msgid "" +"Record and store microphone audio while driving. The audio will be included " +"in the dashcam video in comma connect." +msgstr "" +"行車時錄製並儲存麥克風音訊。音訊將包含在 comma connect 的行車紀錄影片中。" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "Regulatory" +msgstr "法規" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Relaxed" +msgstr "從容" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote access" +msgstr "遠端存取" + +#: selfdrive/ui/widgets/prime.py:47 +#, python-format +msgid "Remote snapshots" +msgstr "遠端擷圖" + +#: selfdrive/ui/widgets/ssh_key.py:123 +#, python-format +msgid "Request timed out" +msgstr "要求逾時" + +#: selfdrive/ui/layouts/settings/device.py:119 +#, python-format +msgid "Reset" +msgstr "重設" + +#: selfdrive/ui/layouts/settings/device.py:51 +#, python-format +msgid "Reset Calibration" +msgstr "重設校正" + +#: selfdrive/ui/layouts/settings/device.py:65 +#, python-format +msgid "Review Training Guide" +msgstr "檢視訓練指南" + +#: selfdrive/ui/layouts/settings/device.py:27 +msgid "Review the rules, features, and limitations of openpilot" +msgstr "檢視 openpilot 的規則、功能與限制" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "SELECT" +msgstr "選取" + +#: selfdrive/ui/layouts/settings/developer.py:53 +#, python-format +msgid "SSH Keys" +msgstr "SSH 金鑰" + +#: system/ui/widgets/network.py:310 +#, python-format +msgid "Scanning Wi-Fi networks..." +msgstr "正在掃描 Wi‑Fi 網路…" + +#: system/ui/widgets/option_dialog.py:36 +#, python-format +msgid "Select" +msgstr "選取" + +#: selfdrive/ui/layouts/settings/software.py:183 +#, python-format +msgid "Select a branch" +msgstr "選取分支" + +#: selfdrive/ui/layouts/settings/device.py:91 +#, python-format +msgid "Select a language" +msgstr "選取語言" + +#: selfdrive/ui/layouts/settings/device.py:60 +#, python-format +msgid "Serial" +msgstr "序號" + +#: selfdrive/ui/widgets/offroad_alerts.py:106 +#, python-format +msgid "Snooze Update" +msgstr "延後更新" + +#: selfdrive/ui/layouts/settings/settings.py:65 +msgid "Software" +msgstr "軟體" + +#: selfdrive/ui/layouts/settings/toggles.py:98 +#, python-format +msgid "Standard" +msgstr "標準" + +#: selfdrive/ui/layouts/settings/toggles.py:22 +msgid "" +"Standard is recommended. In aggressive mode, openpilot will follow lead cars " +"closer and be more aggressive with the gas and brake. In relaxed mode " +"openpilot will stay further away from lead cars. On supported cars, you can " +"cycle through these personalities with your steering wheel distance button." +msgstr "" +"建議使用標準模式。積極模式下,openpilot 會更貼近前車,油門與煞車反應更積極;" +"從容模式下,會與前車保持更遠距離。於支援車款,可用方向盤距離按鈕切換這些風" +"格。" + +#: selfdrive/ui/onroad/alert_renderer.py:59 +#: selfdrive/ui/onroad/alert_renderer.py:65 +#, python-format +msgid "System Unresponsive" +msgstr "系統無回應" + +#: selfdrive/ui/onroad/alert_renderer.py:58 +#, python-format +msgid "TAKE CONTROL IMMEDIATELY" +msgstr "請立刻接手控制" + +#: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 +#: selfdrive/ui/layouts/sidebar.py:127 selfdrive/ui/layouts/sidebar.py:129 +msgid "TEMP" +msgstr "溫度" + +#: selfdrive/ui/layouts/settings/software.py:61 +#, python-format +msgid "Target Branch" +msgstr "目標分支" + +#: system/ui/widgets/network.py:124 +#, python-format +msgid "Tethering Password" +msgstr "網路共享密碼" + +#: selfdrive/ui/layouts/settings/settings.py:64 +msgid "Toggles" +msgstr "切換" + +#: selfdrive/ui/layouts/settings/software.py:72 +#, python-format +msgid "UNINSTALL" +msgstr "解除安裝" + +#: selfdrive/ui/layouts/home.py:155 +#, python-format +msgid "UPDATE" +msgstr "更新" + +#: selfdrive/ui/layouts/settings/software.py:72 +#: selfdrive/ui/layouts/settings/software.py:163 +#, python-format +msgid "Uninstall" +msgstr "解除安裝" + +#: selfdrive/ui/layouts/sidebar.py:117 +msgid "Unknown" +msgstr "未知" + +#: selfdrive/ui/layouts/settings/software.py:48 +#, python-format +msgid "Updates are only downloaded while the car is off." +msgstr "僅在車輛熄火時下載更新。" + +#: selfdrive/ui/widgets/prime.py:33 +#, python-format +msgid "Upgrade Now" +msgstr "立即升級" + +#: selfdrive/ui/layouts/settings/toggles.py:31 +msgid "" +"Upload data from the driver facing camera and help improve the driver " +"monitoring algorithm." +msgstr "上傳車內鏡頭資料,協助改善駕駛監控演算法。" + +#: selfdrive/ui/layouts/settings/toggles.py:88 +#, python-format +msgid "Use Metric System" +msgstr "使用公制" + +#: selfdrive/ui/layouts/settings/toggles.py:17 +msgid "" +"Use the openpilot system for adaptive cruise control and lane keep driver " +"assistance. Your attention is required at all times to use this feature." +msgstr "" +"使用 openpilot 進行 ACC 與車道維持輔助。使用此功能時,您必須始終保持專注。" + +#: selfdrive/ui/layouts/sidebar.py:72 selfdrive/ui/layouts/sidebar.py:144 +msgid "VEHICLE" +msgstr "車輛" + +#: selfdrive/ui/layouts/settings/device.py:67 +#, python-format +msgid "VIEW" +msgstr "檢視" + +#: selfdrive/ui/onroad/alert_renderer.py:52 +#, python-format +msgid "Waiting to start" +msgstr "等待開始" + +#: selfdrive/ui/layouts/settings/developer.py:19 +msgid "" +"Warning: This grants SSH access to all public keys in your GitHub settings. " +"Never enter a GitHub username other than your own. A comma employee will " +"NEVER ask you to add their GitHub username." +msgstr "" +"警告:這將授予對您 GitHub 設定中所有公開金鑰的 SSH 存取權。請勿輸入非您本人" +"的 GitHub 帳號。comma 員工絕不會要求您新增他們的帳號。" + +#: selfdrive/ui/layouts/onboarding.py:111 +#, python-format +msgid "Welcome to openpilot" +msgstr "歡迎使用 openpilot" + +#: selfdrive/ui/layouts/settings/toggles.py:20 +msgid "When enabled, pressing the accelerator pedal will disengage openpilot." +msgstr "啟用後,踩下加速踏板將會脫離 openpilot。" + +#: selfdrive/ui/layouts/sidebar.py:44 +msgid "Wi-Fi" +msgstr "Wi‑Fi" + +#: system/ui/widgets/network.py:144 +#, python-format +msgid "Wi-Fi Network Metered" +msgstr "Wi‑Fi 計量網路" + +#: system/ui/widgets/network.py:314 +#, python-format +msgid "Wrong password" +msgstr "密碼錯誤" + +#: selfdrive/ui/layouts/onboarding.py:145 +#, python-format +msgid "You must accept the Terms and Conditions in order to use openpilot." +msgstr "您必須接受條款與細則才能使用 openpilot。" + +#: selfdrive/ui/layouts/onboarding.py:112 +#, python-format +msgid "" +"You must accept the Terms and Conditions to use openpilot. Read the latest " +"terms at https://comma.ai/terms before continuing." +msgstr "" +"您必須接受條款與細則才能使用 openpilot。繼續前請閱讀 https://comma.ai/terms " +"上的最新條款。" + +#: selfdrive/ui/onroad/driver_camera_dialog.py:34 +#, python-format +msgid "camera starting" +msgstr "相機啟動中" + +#: selfdrive/ui/widgets/prime.py:63 +#, python-format +msgid "comma prime" +msgstr "comma prime" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "default" +msgstr "預設" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "down" +msgstr "下" + +#: selfdrive/ui/layouts/settings/software.py:106 +#, python-format +msgid "failed to check for update" +msgstr "檢查更新失敗" + +#: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 +#, python-format +msgid "for \"{}\"" +msgstr "適用於「{}」" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "km/h" +msgstr "公里/時" + +#: system/ui/widgets/network.py:204 +#, python-format +msgid "leave blank for automatic configuration" +msgstr "留空以自動設定" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "left" +msgstr "左" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "metered" +msgstr "計量" + +#: selfdrive/ui/onroad/hud_renderer.py:177 +#, python-format +msgid "mph" +msgstr "英里/時" + +#: selfdrive/ui/layouts/settings/software.py:20 +#, python-format +msgid "never" +msgstr "從不" + +#: selfdrive/ui/layouts/settings/software.py:31 +#, python-format +msgid "now" +msgstr "現在" + +#: selfdrive/ui/layouts/settings/developer.py:71 +#, python-format +msgid "openpilot Longitudinal Control (Alpha)" +msgstr "openpilot 縱向控制(Alpha)" + +#: selfdrive/ui/onroad/alert_renderer.py:51 +#, python-format +msgid "openpilot Unavailable" +msgstr "openpilot 無法使用" + +#: selfdrive/ui/layouts/settings/toggles.py:158 +#, python-format +msgid "" +"openpilot defaults to driving in chill mode. Experimental mode enables alpha-" +"level features that aren't ready for chill mode. Experimental features are " +"listed below:

End-to-End Longitudinal Control


Let the driving " +"model control the gas and brakes. openpilot will drive as it thinks a human " +"would, including stopping for red lights and stop signs. Since the driving " +"model decides the speed to drive, the set speed will only act as an upper " +"bound. This is an alpha quality feature; mistakes should be expected." +"

New Driving Visualization


The driving visualization will " +"transition to the road-facing wide-angle camera at low speeds to better show " +"some turns. The Experimental mode logo will also be shown in the top right " +"corner." +msgstr "" +"openpilot 預設以安穩模式行駛。實驗模式啟用尚未準備好進入安穩模式的 Alpha 等級" +"功能。實驗功能如下:

端到端縱向控制


讓駕駛模型控制油門與煞車。" +"openpilot 會如同人類駕駛般行駛,包括在紅燈與停車標誌前停車。由於駕駛模型決定" +"行駛速度,設定速度僅作為上限。此為 Alpha 品質功能;預期會有失誤。

全新" +"駕駛視覺化


在低速時,駕駛視覺化將切換至面向道路的廣角鏡頭以更好呈現部" +"分轉彎。右上角亦會顯示實驗模式圖示。" + +#: selfdrive/ui/layouts/settings/device.py:165 +#, python-format +msgid "" +"openpilot is continuously calibrating, resetting is rarely required. " +"Resetting calibration will restart openpilot if the car is powered on." +msgstr "" +"openpilot 會持續校正,通常不需重設。若車輛通電,重設校正將重新啟動 " +"openpilot。" + +#: selfdrive/ui/layouts/settings/firehose.py:20 +msgid "" +"openpilot learns to drive by watching humans, like you, drive.\n" +"\n" +"Firehose Mode allows you to maximize your training data uploads to improve " +"openpilot's driving models. More data means bigger models, which means " +"better Experimental Mode." +msgstr "" +"openpilot 透過觀察人類(也就是您)的駕駛方式來學習。\n" +"\n" +"Firehose 模式可讓您最大化上傳訓練資料,以改進 openpilot 的駕駛模型。更多資料" +"代表更大的模型,也就代表更好的實驗模式。" + +#: selfdrive/ui/layouts/settings/toggles.py:183 +#, python-format +msgid "openpilot longitudinal control may come in a future update." +msgstr "openpilot 縱向控制可能於未來更新提供。" + +#: selfdrive/ui/layouts/settings/device.py:26 +msgid "" +"openpilot requires the device to be mounted within 4° left or right and " +"within 5° up or 9° down." +msgstr "openpilot 要求裝置安裝在左右 4°、上 5° 或下 9° 以內。" + +#: selfdrive/ui/layouts/settings/device.py:134 +#, python-format +msgid "right" +msgstr "右" + +#: system/ui/widgets/network.py:142 +#, python-format +msgid "unmetered" +msgstr "不限流量" + +#: selfdrive/ui/layouts/settings/device.py:133 +#, python-format +msgid "up" +msgstr "上" + +#: selfdrive/ui/layouts/settings/software.py:117 +#, python-format +msgid "up to date, last checked never" +msgstr "已為最新,最後檢查:從未" + +#: selfdrive/ui/layouts/settings/software.py:115 +#, python-format +msgid "up to date, last checked {}" +msgstr "已為最新,最後檢查:{}" + +#: selfdrive/ui/layouts/settings/software.py:109 +#, python-format +msgid "update available" +msgstr "有可用更新" + +#: selfdrive/ui/layouts/home.py:169 +#, python-format +msgid "{} ALERT" +msgid_plural "{} ALERTS" +msgstr[0] "{} 則警示" +msgstr[1] "{} 則警示" + +#: selfdrive/ui/layouts/settings/software.py:40 +#, python-format +msgid "{} day ago" +msgid_plural "{} days ago" +msgstr[0] "{} 天前" +msgstr[1] "{} 天前" + +#: selfdrive/ui/layouts/settings/software.py:37 +#, python-format +msgid "{} hour ago" +msgid_plural "{} hours ago" +msgstr[0] "{} 小時前" +msgstr[1] "{} 小時前" + +#: selfdrive/ui/layouts/settings/software.py:34 +#, python-format +msgid "{} minute ago" +msgid_plural "{} minutes ago" +msgstr[0] "{} 分鐘前" +msgstr[1] "{} 分鐘前" + +#: selfdrive/ui/layouts/settings/firehose.py:111 +#, python-format +msgid "{} segment of your driving is in the training dataset so far." +msgid_plural "{} segments of your driving is in the training dataset so far." +msgstr[0] "目前已有 {} 個您的駕駛片段納入訓練資料集。" +msgstr[1] "目前已有 {} 個您的駕駛片段納入訓練資料集。" + +#: selfdrive/ui/widgets/prime.py:62 +#, python-format +msgid "✓ SUBSCRIBED" +msgstr "✓ 已訂閱" + +#: selfdrive/ui/widgets/setup.py:22 +#, python-format +msgid "🔥 Firehose Mode 🔥" +msgstr "🔥 Firehose 模式 🔥" diff --git a/selfdrive/ui/translations/auto_translate.py b/selfdrive/ui/translations/auto_translate.py index c2e4bbc552..6251e03397 100755 --- a/selfdrive/ui/translations/auto_translate.py +++ b/selfdrive/ui/translations/auto_translate.py @@ -26,7 +26,7 @@ def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]: for filename in language_dict.values(): path = TRANSLATIONS_DIR / f"{filename}.ts" - language = path.stem.split("main_")[1] + language = path.stem if languages is None or language in languages: files[language] = path diff --git a/selfdrive/ui/translations/create_badges.py b/selfdrive/ui/translations/create_badges.py index 3e14c33255..2948c4857d 100755 --- a/selfdrive/ui/translations/create_badges.py +++ b/selfdrive/ui/translations/create_badges.py @@ -5,13 +5,66 @@ import requests import xml.etree.ElementTree as ET from openpilot.common.basedir import BASEDIR -from openpilot.selfdrive.ui.tests.test_translations import UNFINISHED_TRANSLATION_TAG from openpilot.selfdrive.ui.update_translations import LANGUAGES_FILE, TRANSLATIONS_DIR -TRANSLATION_TAG = " 2: + has_content = True + break + # End of entry + if stripped.startswith(('msgid', '#')) or not stripped: + break + + if not has_content: + unfinished_translations += 1 + + return (total_translations, unfinished_translations) + if __name__ == "__main__": with open(LANGUAGES_FILE) as f: translation_files = json.load(f) @@ -19,18 +72,11 @@ if __name__ == "__main__": badge_svg = [] max_badge_width = 0 # keep track of max width to set parent element for idx, (name, file) in enumerate(translation_files.items()): - with open(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")) as tr_f: - tr_file = tr_f.read() + po_file_path = os.path.join(str(TRANSLATIONS_DIR), f"app_{file}.po") - total_translations = 0 - unfinished_translations = 0 - for line in tr_file.splitlines(): - if TRANSLATION_TAG in line: - total_translations += 1 - if UNFINISHED_TRANSLATION_TAG in line: - unfinished_translations += 1 + total_translations, unfinished_translations = parse_po_file(po_file_path) - percent_finished = int(100 - (unfinished_translations / total_translations * 100.)) + percent_finished = int(100 - (unfinished_translations / total_translations * 100.)) if total_translations > 0 else 0 color = f"rgb{(94, 188, 0) if percent_finished == 100 else (248, 255, 50) if percent_finished > 90 else (204, 55, 27)}" # Download badge diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json index 132b5088d7..b0674dee82 100644 --- a/selfdrive/ui/translations/languages.json +++ b/selfdrive/ui/translations/languages.json @@ -1,14 +1,14 @@ { - "English": "main_en", - "Deutsch": "main_de", - "Français": "main_fr", - "Português": "main_pt-BR", - "Español": "main_es", - "Türkçe": "main_tr", - "العربية": "main_ar", - "ไทย": "main_th", - "中文(繁體)": "main_zh-CHT", - "中文(简体)": "main_zh-CHS", - "한국어": "main_ko", - "日本語": "main_ja" + "English": "en", + "Deutsch": "de", + "Français": "fr", + "Português": "pt-BR", + "Español": "es", + "Türkçe": "tr", + "العربية": "ar", + "ไทย": "th", + "中文(繁體)": "zh-CHT", + "中文(简体)": "zh-CHS", + "한국어": "ko", + "日本語": "ja" } diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts deleted file mode 100644 index 4048daf74a..0000000000 --- a/selfdrive/ui/translations/main_ar.ts +++ /dev/null @@ -1,2869 +0,0 @@ - - - - - AbstractAlert - - Close - إغلاق - - - Reboot and Update - إعادة التشغيل والتحديث - - - - AdvancedNetworking - - Back - السابق - - - Enable Tethering - تمكين الربط - - - Tethering Password - كلمة مرور الربط - - - EDIT - تعديل - - - Enter new tethering password - أدخل كلمة مرور الربط الجديدة - - - IP Address - عنوان IP - - - Enable Roaming - تمكين التجوال - - - APN Setting - إعدادات APN - - - Enter APN - إدخال APN - - - leave blank for automatic configuration - اتركه فارغاً من أجل التكوين التلقائي - - - Cellular Metered - محدود بالاتصال الخلوي - - - Hidden Network - شبكة مخفية - - - CONNECT - الاتصال - - - Enter SSID - أدخل SSID - - - Enter password - أدخل كلمة المرور - - - for "%1" - من أجل "%1" - - - Prevent large data uploads when on a metered cellular connection - - - - default - - - - metered - - - - unmetered - - - - Wi-Fi Network Metered - - - - Prevent large data uploads when on a metered Wi-Fi connection - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - موافق - - - Cancel - إلغاء - - - - DeclinePage - - Back - السابق - - - Decline, uninstall %1 - رفض، إلغاء التثبيت %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - وضع تصحيح أخطاء عصا التحكم - - - Longitudinal Maneuver Mode - وضع المناورة الطولية - - - openpilot Longitudinal Control (Alpha) - التحكم الطولي openpilot (ألفا) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - تحذير: التحكم الطولي في openpilot في المرحلة ألفا لهذه السيارة، وسيقوم بتعطيل مكابح الطوارئ الآلية (AEB). - - - Enable ADB - تمكين ADB - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - أداة ADB (Android Debug Bridge) تسمح بالاتصال بجهازك عبر USB أو عبر الشبكة. راجع هذا الرابط: https://docs.comma.ai/how-to/connect-to-comma لمزيد من المعلومات. - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - عرض - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - معرف دونجل - - - N/A - غير متاح - - - Serial - الرقم التسلسلي - - - Driver Camera - كاميرة السائق - - - PREVIEW - معاينة - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - قم بمعاينة الكاميرا المواجهة للسائق للتأكد من أن نظام مراقبة السائق يتمتع برؤية جيدة. (يجب أن تكون السيارة متوقفة) - - - Reset Calibration - إعادة ضبط المعايرة - - - RESET - إعادة الضبط - - - Are you sure you want to reset calibration? - هل أنت متأكد أنك تريد إعادة ضبط المعايرة؟ - - - Review Training Guide - مراجعة دليل التدريب - - - REVIEW - مراجعة - - - Are you sure you want to review the training guide? - هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ - - - Regulatory - التنظيمية - - - VIEW - عرض - - - Change Language - تغيير اللغة - - - CHANGE - تغيير - - - Select a language - اختر لغة - - - Reboot - إعادة التشغيل - - - Power Off - إيقاف التشغيل - - - Your device is pointed %1° %2 and %3° %4. - يشير جهازك إلى %1 درجة %2، و%3 درجة %4. - - - down - نحو الأسفل - - - up - نحو الأعلى - - - left - نحو اليسار - - - right - نحو اليمين - - - Are you sure you want to reboot? - هل أنت متأكد أنك تريد إعادة التشغيل؟ - - - Disengage to Reboot - فك الارتباط من أجل إعادة التشغيل - - - Are you sure you want to power off? - هل أنت متأكد أنك تريد إيقاف التشغيل؟ - - - Disengage to Power Off - فك الارتباط من أجل إيقاف التشغيل - - - Reset - إعادة الضبط - - - Review - مراجعة - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - اقرن جهازك بجهاز (connect.comma.ai) واحصل على عرضك من comma prime. - - - Pair Device - إقران الجهاز - - - PAIR - إقران - - - Disengage to Reset Calibration - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - التنظيمية - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ - - - Review - مراجعة - - - Select a language - اختر لغة - - - Wake-Up Behavior - - - - Reboot - إعادة التشغيل - - - Power Off - إيقاف التشغيل - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - Are you sure you want to enter Always Offroad mode? - - - - Disengage to Enter Always Offroad Mode - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - بدء تشغيل الكاميرا - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - تشغيل الوضع التجريبي - - - CHILL MODE ON - تشغيل وضع الراحة - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - التحقق - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - وضع خرطوم الحريق: نشط - - - ACTIVE - نشط - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك في مجموعة بيانات التدريب. - حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك في مجموعة بيانات التدريب. - حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك في مجموعة بيانات التدريب. - حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك في مجموعة بيانات التدريب. - حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك في مجموعة بيانات التدريب. - حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك في مجموعة بيانات التدريب. - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode - - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - كم/س - - - mph - ميل/س - - - MAX - MAX - - - - HudRendererSP - - km/h - كم/س - - - mph - ميل/س - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - MAX - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - إلغاء - - - Need at least %n character(s)! - - تحتاج إلى حرف %n على الأقل! - تحتاج إلى حرف %n على الأقل! - تحتاج إلى حرفين %n على الأقل! - تحتاج إلى %n أحرف على الأقل! - تحتاج إلى %n أحرف على الأقل! - تحتاج إلى %n حرف على الأقل! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - اختيار - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - إعادة ضبط المعايرة - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - متابعة - - - on Metered - - - - Cancel - إلغاء - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - اختيار - - - Cancel - إلغاء - - - - Networking - - Advanced - متقدم - - - Enter password - أدخل كلمة المرور - - - for "%1" - من أجل "%1" - - - Wrong password - كلمة مرور خاطئة - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - درجة حرارة الجهاز مرتفعة جداً. يقوم النظام بالتبريد قبل البدء. درجة الحرارة الحالية للمكونات الداخلية: %1 - - - Unable to download updates -%1 - غير قادر على تحميل التحديثات -%1 - - - Taking camera snapshots. System won't start until finished. - التقاط لقطات كاميرا. لن يبدأ النظام حتى تنتهي هذه العملية. - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - يتم تنزيل تحديث لنظام تشغيل جهازك في الخلفية. سيطلَب منك التحديث عندما يصبح جاهزاً للتثبيت. - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - - - - Acknowledge Excessive Actuation - - - - Snooze Update - تأخير التحديث - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - تحديث - - - ALERTS - التنبهات - - - ALERT - تنبيه - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - تحكم على الفور - - - Reboot Device - إعادة التشغيل - - - Waiting to start - في انتظار البدء - - - System Unresponsive - النظام لا يستجيب - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - التحقق - - - Country - - - - SELECT - اختيار - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - تحديث - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - اقرن جهازك مع حسابك على comma - - - Go to https://connect.comma.ai on your phone - انتقل إلى https://connect.comma.ai على جوالك - - - Click "add new device" and scan the QR code on the right - انقر "،إضافة جهاز جديد"، وامسح رمز الاستجابة السريعة (QR) على اليمين - - - Bookmark connect.comma.ai to your home screen to use it like an app - اجعل لـconnect.comma.ai إشارة مرجعية على شاشتك الرئيسية من أجل استخدامه مثل أي تطبيق - - - Please connect to Wi-Fi to complete initial pairing - يرجى الاتصال بشبكة الواي فاي لإكمال الاقتران الأولي - - - - ParamControl - - Enable - تمكين - - - Cancel - إلغاء - - - - ParamControlSP - - Enable - تمكين - - - Cancel - إلغاء - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - إزالة - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - إلغاء - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - الترقية الآن - - - Become a comma prime member at connect.comma.ai - كن عضوًا في comma prime على connect.comma.ai - - - PRIME FEATURES: - الميزات الأساسية: - - - Remote access - التحكم عن بعد - - - 24/7 LTE connectivity - اتصال LTE على مدار الساعة 24/7 - - - 1 year of drive storage - سنة واحدة من تخزين القرص - - - Remote snapshots - لقطات عن بُعد - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ مشترك - - - comma prime - comma prime - - - - QObject - - %n minute(s) ago - - منذ %n دقيقة - منذ %n دقيقة - منذ دقيقتين %n - منذ %n دقائق - منذ %n دقائق - منذ %n دقيقة - - - - %n hour(s) ago - - منذ %n ساعة - منذ %n ساعة - منذ ساعتين %n - منذ %n ساعات - منذ %n ساعات - منذ %n ساعة - - - - %n day(s) ago - - منذ %n يوم - منذ %n يوم - منذ يومين %n - منذ %n أيام - منذ %n أيام - منذ %n يوم - - - - now - الآن - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - الجهاز - - - Network - الشبكة - - - Toggles - المثبتتات - - - Software - البرنامج - - - Developer - المطور - - - Firehose - خرطوم الحريق - - - - SettingsWindowSP - - × - × - - - Device - الجهاز - - - Network - الشبكة - - - sunnylink - - - - Toggles - المثبتتات - - - Software - البرنامج - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - خرطوم الحريق - - - Developer - المطور - - - Display - - - - - SetupWidget - - Finish Setup - إنهاء الإعداد - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - اقرن جهازك بجهاز (connect.comma.ai) واحصل على عرضك من comma prime. - - - Pair device - اقتران الجهاز - - - - Sidebar - - CONNECT - الاتصال - - - OFFLINE - غير متصل - - - ONLINE - متصل - - - ERROR - خطأ - - - TEMP - درجة الحرارة - - - HIGH - مرتفع - - - GOOD - جيد - - - OK - موافق - - - VEHICLE - المركبة - - - NO - لا - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - غير متصل - - - REGIST... - - - - ONLINE - متصل - - - ERROR - خطأ - - - SUNNYLINK - - - - - SoftwarePanel - - UNINSTALL - إلغاء التثبيت - - - Uninstall %1 - إلغاء التثبيت %1 - - - Are you sure you want to uninstall? - هل أنت متأكد أنك تريد إلغاء التثبيت؟ - - - CHECK - التحقق - - - Updates are only downloaded while the car is off. - يتم تحميل التحديثات فقط عندما تكون السيارة متوقفة. - - - Current Version - النسخة الحالية - - - Download - تنزيل - - - Install Update - تثبيت التحديث - - - INSTALL - تثبيت - - - Target Branch - فرع الهدف - - - SELECT - اختيار - - - Select a branch - اختر فرعاً - - - Uninstall - إلغاء التثبيت - - - failed to check for update - فشل التحقق من التحديث - - - DOWNLOAD - تنزيل - - - update available - يتوفر تحديث - - - never - إطلاقاً - - - up to date, last checked %1 - أحدث نسخة، آخر تحقق %1 - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - اختر فرعاً - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - إعادة التشغيل - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - السابق - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - السابق - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - مفاتيح SSH - - - 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. - تنبيه: هذا يمنح SSH إمكانية الوصول إلى جميع المفاتيح العامة في إعدادات GitHub. لا تقم بإدخال اسم مستخدم GitHub بدلاً من اسمك. لن تطلب منك comma employee إطلاقاً أن تضيف اسم مستخدم GitHub الخاص بهم. - - - ADD - إضافة - - - Enter your GitHub username - ادخل اسم المستخدم GitHub الخاص بك - - - LOADING - يتم التحميل - - - REMOVE - إزالة - - - Username '%1' has no keys on GitHub - لا يحتوي اسم المستخدم '%1' أي مفاتيح على GitHub - - - Request timed out - انتهى وقت الطلب - - - Username '%1' doesn't exist on GitHub - اسم المستخدم '%1' غير موجود على GitHub - - - - SshToggle - - Enable SSH - تمكين SSH - - - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - Enable sunnylink - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - إقران - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - غير متاح - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - رفض - - - Agree - أوافق - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - قم بتمكين تحذيرات مغادرة المسار - - - Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). - تلقي التنبيهات من أجل الالتفاف للعودة إلى المسار عندما تنحرف سيارتك فوق الخط المحدد للمسار دون تشغيل إشارة الانعطاف عند القيادة لمسافة تزيد عن 31 ميل/سا (50 كم/سا). - - - Use Metric System - استخدام النظام المتري - - - Display speed in km/h instead of mph. - عرض السرعة بواحدات كم/سا بدلاً من ميل/سا. - - - Record and Upload Driver Camera - تسجيل وتحميل كاميرا السائق - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - تحميل البيانات من الكاميرا المواجهة للسائق، والمساعدة في تحسين خوارزمية مراقبة السائق. - - - Disengage on Accelerator Pedal - فك الارتباط عن دواسة الوقود - - - Experimental Mode - الوضع التجريبي - - - Aggressive - الهجومي - - - Standard - القياسي - - - Relaxed - الراحة - - - Driving Personality - شخصية القيادة - - - End-to-End Longitudinal Control - التحكم الطولي من طرف إلى طرف - - - New Driving Visualization - تصور القيادة الديد - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - الوضع التجريبي غير متوفر حالياً في هذه السيارة نظراً لاستخدام رصيد التحكم التكيفي بالسرعة من أجل التحكم الطولي. - - - openpilot longitudinal control may come in a future update. - قد يتم الحصول على التحكم الطولي في openpilot في عمليات التحديث المستقبلية. - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - ستتحول واجهة القيادة إلى الكاميرا الواسعة المواجهة للطريق عند السرعات المنخفضة لعرض بعض المنعطفات بشكل أفضل. كما سيتم عرض شعار وضع التجريبي في الزاوية العلوية اليمنى. - - - Always-On Driver Monitoring - مراقبة السائق المستمرة - - - Changing this setting will restart openpilot if the car is powered on. - - - - Record and Upload Microphone Audio - - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - اختيار - - - Cancel - إلغاء - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - 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> وضع خرطوم الحريق <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - يتم البحث عن شبكات... - - - CONNECTING... - يتم الاتصال... - - - FORGET - نسيان هذه الشبكة - - - Forget Wi-Fi Network "%1"? - هل تريد نسيان شبكة الواي فاي "%1"؟ - - - Forget - نسيان - - - diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts deleted file mode 100644 index 3d90dca182..0000000000 --- a/selfdrive/ui/translations/main_de.ts +++ /dev/null @@ -1,2851 +0,0 @@ - - - - - AbstractAlert - - Close - Schließen - - - Reboot and Update - Aktualisieren und neu starten - - - - AdvancedNetworking - - Back - Zurück - - - Enable Tethering - Tethering aktivieren - - - Tethering Password - Tethering Passwort - - - EDIT - ÄNDERN - - - Enter new tethering password - Neues tethering Passwort eingeben - - - IP Address - IP Adresse - - - Enable Roaming - Roaming aktivieren - - - APN Setting - APN Einstellungen - - - Enter APN - APN eingeben - - - leave blank for automatic configuration - für automatische Konfiguration leer lassen - - - Cellular Metered - Getaktete Verbindung - - - Hidden Network - Verborgenes Netzwerk - - - CONNECT - VERBINDEN - - - Enter SSID - SSID eingeben - - - Enter password - Passwort eingeben - - - for "%1" - für "%1" - - - Prevent large data uploads when on a metered cellular connection - - - - default - - - - metered - - - - unmetered - - - - Wi-Fi Network Metered - - - - Prevent large data uploads when on a metered Wi-Fi connection - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - Ok - - - Cancel - Abbrechen - - - - DeclinePage - - Back - Zurück - - - Decline, uninstall %1 - Ablehnen, deinstallieren %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - Joystick Debug-Modus - - - Longitudinal Maneuver Mode - Längsmanöver-Modus - - - openpilot Longitudinal Control (Alpha) - openpilot Längsregelung (Alpha) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - WARNUNG: Die openpilot Längsregelung befindet sich für dieses Fahrzeug im Alpha-Stadium und deaktiviert das automatische Notbremsen (AEB). - - - Enable ADB - ADB aktivieren - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB (Android Debug Bridge) ermöglicht die Verbindung zu deinem Gerät über USB oder Netzwerk. Siehe https://docs.comma.ai/how-to/connect-to-comma für weitere Informationen. - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - ANSEHEN - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - Dongle ID - - - N/A - Nicht verfügbar - - - Serial - Seriennummer - - - Driver Camera - Fahrerkamera - - - PREVIEW - VORSCHAU - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - Vorschau der auf den Fahrer gerichteten Kamera, um sicherzustellen, dass die Fahrerüberwachung eine gute Sicht hat. (Fahrzeug muss aus sein) - - - Reset Calibration - Neu kalibrieren - - - RESET - RESET - - - Are you sure you want to reset calibration? - Bist du sicher, dass du die Kalibrierung zurücksetzen möchtest? - - - Review Training Guide - Trainingsanleitung wiederholen - - - REVIEW - TRAINING - - - Are you sure you want to review the training guide? - Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? - - - Regulatory - Rechtliche Hinweise - - - VIEW - ANSEHEN - - - Change Language - Sprache ändern - - - CHANGE - ÄNDERN - - - Select a language - Sprache wählen - - - Reboot - Neustart - - - Power Off - Ausschalten - - - Your device is pointed %1° %2 and %3° %4. - Deine Geräteausrichtung ist %1° %2 und %3° %4. - - - down - unten - - - up - oben - - - left - links - - - right - rechts - - - Are you sure you want to reboot? - Bist du sicher, dass du das Gerät neu starten möchtest? - - - Disengage to Reboot - Für Neustart deaktivieren - - - Are you sure you want to power off? - Bist du sicher, dass du das Gerät ausschalten möchtest? - - - Disengage to Power Off - Zum Ausschalten deaktivieren - - - Reset - Zurücksetzen - - - Review - Überprüfen - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Koppele dein Gerät mit Comma Connect (connect.comma.ai) und sichere dir dein Comma Prime Angebot. - - - Pair Device - Gerät koppeln - - - PAIR - KOPPELN - - - Disengage to Reset Calibration - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - Rechtliche Hinweise - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? - - - Review - Überprüfen - - - Select a language - Sprache wählen - - - Wake-Up Behavior - - - - Reboot - Neustart - - - Power Off - Ausschalten - - - 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 - - - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. - - - - Reset - Zurücksetzen - - - The reset cannot be undone. You have been warned. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - Kamera startet - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - EXPERIMENTELLER MODUS AN - - - CHILL MODE ON - ENTSPANNTER MODUS AN - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - ÜBERPRÜFEN - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - Firehose-Modus: AKTIV - - - ACTIVE - AKTIV - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>%n Segment</b> deiner Fahrten ist bisher im Trainingsdatensatz. - <b>%n Segmente</b> deiner Fahrten sind bisher im Trainingsdatensatz. - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INAKTIV</span>: Verbinde dich mit einem ungedrosselten Netzwerk - - - Firehose Mode - - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - MAX - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - MAX - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - Abbrechen - - - Need at least %n character(s)! - - Mindestens %n Buchstabe benötigt! - Mindestens %n Buchstaben benötigt! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - AUSWÄHLEN - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Neu kalibrieren - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - Fortsetzen - - - on Metered - - - - Cancel - Abbrechen - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - Auswählen - - - Cancel - Abbrechen - - - - Networking - - Advanced - Erweitert - - - Enter password - Passwort eingeben - - - for "%1" - für "%1" - - - Wrong password - Falsches Passwort - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Unable to download updates -%1 - Updates konnten nicht heruntergeladen werden -%1 - - - Taking camera snapshots. System won't start until finished. - Kamera-Snapshots werden aufgenommen. Das System startet erst, wenn dies abgeschlossen ist. - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - Ein Update für das Betriebssystem deines Geräts wird im Hintergrund heruntergeladen. Du wirst aufgefordert, das Update zu installieren, sobald es bereit ist. - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - Gerätetemperatur zu hoch. Das System kühlt ab, bevor es startet. Aktuelle interne Komponententemperatur: %1 - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - - - - Acknowledge Excessive Actuation - - - - Snooze Update - Update pausieren - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - Aktualisieren - - - ALERTS - HINWEISE - - - ALERT - HINWEIS - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - ÜBERNIMM SOFORT DIE KONTROLLE - - - Reboot Device - Gerät neu starten - - - Waiting to start - Warten auf Start - - - System Unresponsive - System reagiert nicht - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - ÜBERPRÜFEN - - - Country - - - - SELECT - AUSWÄHLEN - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - Aktualisieren - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - Verbinde dein Gerät mit deinem comma Konto - - - Go to https://connect.comma.ai on your phone - Gehe zu https://connect.comma.ai auf deinem Handy - - - Click "add new device" and scan the QR code on the right - Klicke auf "neues Gerät hinzufügen" und scanne den QR code rechts - - - Bookmark connect.comma.ai to your home screen to use it like an app - Füge connect.comma.ai als Lesezeichen auf deinem Homescreen hinzu um es wie eine App zu verwenden - - - Please connect to Wi-Fi to complete initial pairing - Bitte verbinde dich mit WLAN, um die Koppelung abzuschließen. - - - - ParamControl - - Cancel - Abbrechen - - - Enable - Aktivieren - - - - ParamControlSP - - Enable - Aktivieren - - - Cancel - Abbrechen - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - LÖSCHEN - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - Abbrechen - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - Jetzt abonieren - - - Become a comma prime member at connect.comma.ai - Werde Comma Prime Mitglied auf connect.comma.ai - - - PRIME FEATURES: - PRIME FUNKTIONEN: - - - Remote access - Fernzugriff - - - 24/7 LTE connectivity - 24/7 LTE-Verbindung - - - 1 year of drive storage - Fahrdaten-Speicherung für 1 Jahr - - - Remote snapshots - Remote-Snapshots - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ ABBONIERT - - - comma prime - comma prime - - - - QObject - - %n minute(s) ago - - vor %n Minute - vor %n Minuten - - - - %n hour(s) ago - - vor %n Stunde - vor %n Stunden - - - - %n day(s) ago - - vor %n Tag - vor %n Tagen - - - - now - jetzt - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - x - - - Device - Gerät - - - Network - Netzwerk - - - Toggles - Schalter - - - Software - Software - - - Developer - Entwickler - - - Firehose - Firehose - - - - SettingsWindowSP - - × - x - - - Device - Gerät - - - Network - Netzwerk - - - sunnylink - - - - Toggles - Schalter - - - Software - Software - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - Firehose - - - Developer - Entwickler - - - Display - - - - - SetupWidget - - Finish Setup - Einrichtung beenden - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Koppele dein Gerät mit Comma Connect (connect.comma.ai) und sichere dir dein Comma Prime Angebot. - - - Pair device - Gerät koppeln - - - - Sidebar - - CONNECT - This is a brand/service name for comma connect, don't translate - CONNECT - - - OFFLINE - OFFLINE - - - ONLINE - ONLINE - - - ERROR - FEHLER - - - TEMP - TEMP - - - HIGH - HOCH - - - GOOD - GUT - - - OK - OK - - - VEHICLE - FAHRZEUG - - - NO - KEIN - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - WLAN - - - ETH - LAN - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - OFFLINE - - - REGIST... - - - - ONLINE - ONLINE - - - ERROR - FEHLER - - - SUNNYLINK - - - - - SoftwarePanel - - UNINSTALL - Too long for UI - DEINSTALL - - - Uninstall %1 - Deinstalliere %1 - - - Are you sure you want to uninstall? - Bist du sicher, dass du Openpilot entfernen möchtest? - - - CHECK - ÜBERPRÜFEN - - - Updates are only downloaded while the car is off. - Updates werden nur heruntergeladen, wenn das Auto aus ist. - - - Current Version - Aktuelle Version - - - Download - Download - - - Install Update - Update installieren - - - INSTALL - INSTALLIEREN - - - Target Branch - Ziel Branch - - - SELECT - AUSWÄHLEN - - - Select a branch - Wähle einen Branch - - - Uninstall - Deinstallieren - - - failed to check for update - Update-Prüfung fehlgeschlagen - - - up to date, last checked %1 - Auf dem neuesten Stand, zuletzt geprüft am %1 - - - DOWNLOAD - HERUNTERLADEN - - - update available - Update verfügbar - - - never - nie - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - Wähle einen Branch - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - Neustart - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - Zurück - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - Zurück - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - SSH Schlüssel - - - 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. - Warnung: Dies ermöglicht SSH zugriff für alle öffentlichen Schlüssel in deinen Github Einstellungen. Gib niemals einen anderen Benutzernamen, als deinen Eigenen an. Comma Angestellte fragen dich niemals danach ihren Github Benutzernamen hinzuzufügen. - - - ADD - HINZUFÜGEN - - - Enter your GitHub username - Gib deinen GitHub Benutzernamen ein - - - LOADING - LADEN - - - REMOVE - LÖSCHEN - - - Username '%1' has no keys on GitHub - Benutzername '%1' hat keine Schlüssel auf GitHub - - - Request timed out - Zeitüberschreitung der Anforderung - - - Username '%1' doesn't exist on GitHub - Benutzername '%1' existiert nicht auf GitHub - - - - SshToggle - - Enable SSH - SSH aktivieren - - - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - Enable sunnylink - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - KOPPELN - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - Nicht verfügbar - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - Ablehnen - - - Agree - Zustimmen - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - Spurverlassenswarnungen aktivieren - - - 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). - Erhalte Warnungen, zurück in die Spur zu lenken, wenn dein Auto über eine erkannte Fahrstreifenmarkierung ohne aktivierten Blinker mit mehr als 50 km/h fährt. - - - Use Metric System - Benutze das metrische System - - - Display speed in km/h instead of mph. - Zeige die Geschwindigkeit in km/h anstatt von mph. - - - Record and Upload Driver Camera - Fahrerkamera aufnehmen und hochladen - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - Lade Daten der Fahreraufmerksamkeitsüberwachungskamera hoch, um die Fahreraufmerksamkeitsüberwachungsalgorithmen zu verbessern. - - - Experimental Mode - Experimenteller Modus - - - Disengage on Accelerator Pedal - Bei Gasbetätigung ausschalten - - - New Driving Visualization - Neue Fahrvisualisierung - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - Der experimentelle Modus ist momentan für dieses Auto nicht verfügbar da es den eingebauten adaptiven Tempomaten des Autos benutzt. - - - Aggressive - Aggressiv - - - Standard - Standard - - - Relaxed - Entspannt - - - Driving Personality - Fahrstil - - - End-to-End Longitudinal Control - Ende-zu-Ende Längsregelung - - - openpilot longitudinal control may come in a future update. - Die openpilot Längsregelung könnte in einem zukünftigen Update verfügbar sein. - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - Die Fahrvisualisierung wechselt bei niedrigen Geschwindigkeiten auf die nach vorne gerichtete Weitwinkelkamera, um Kurven besser darzustellen. Das Logo des Experimentellen Modus wird außerdem oben rechts angezeigt. - - - Always-On Driver Monitoring - Dauerhaft aktive Fahrerüberwachung - - - Changing this setting will restart openpilot if the car is powered on. - - - - Record and Upload Microphone Audio - - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - Auswählen - - - Cancel - Abbrechen - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - WiFiPromptWidget - - Open - Öffnen - - - Maximize your training data uploads to improve openpilot's driving models. - Maximiere deine Trainingsdaten-Uploads, um die Fahrmodelle von openpilot zu verbessern. - - - <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-Modus <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - Suche nach Netzwerken... - - - CONNECTING... - VERBINDEN... - - - FORGET - VERGESSEN - - - Forget Wi-Fi Network "%1"? - WLAN Netzwerk "%1" vergessen? - - - Forget - Vergessen - - - diff --git a/selfdrive/ui/translations/main_en.ts b/selfdrive/ui/translations/main_en.ts deleted file mode 100644 index fbccbedb20..0000000000 --- a/selfdrive/ui/translations/main_en.ts +++ /dev/null @@ -1,48 +0,0 @@ - - - - - FirehosePanel - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>%n segment</b> of your driving is in the training dataset so far. - <b>%n segments</b> of your driving are in the training dataset so far. - - - - - InputDialog - - Need at least %n character(s)! - - Need at least %n character! - Need at least %n characters! - - - - - QObject - - %n minute(s) ago - - %n minute ago - %n minutes ago - - - - %n hour(s) ago - - %n hour ago - %n hours ago - - - - %n day(s) ago - - %n day ago - %n days ago - - - - diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts deleted file mode 100644 index 284f63ebf3..0000000000 --- a/selfdrive/ui/translations/main_es.ts +++ /dev/null @@ -1,2853 +0,0 @@ - - - - - AbstractAlert - - Close - Cerrar - - - Reboot and Update - Reiniciar y Actualizar - - - - AdvancedNetworking - - Back - Volver - - - Enable Tethering - Activar Tether - - - Tethering Password - Contraseña de Tethering - - - EDIT - EDITAR - - - Enter new tethering password - Nueva contraseña de tethering - - - IP Address - Dirección IP - - - Enable Roaming - Activar Roaming - - - APN Setting - Configuración de APN - - - Enter APN - Insertar APN - - - leave blank for automatic configuration - dejar en blanco para configuración automática - - - Cellular Metered - Plano de datos limitado - - - Hidden Network - Red Oculta - - - CONNECT - CONECTAR - - - Enter SSID - Ingrese SSID - - - Enter password - Ingrese contraseña - - - for "%1" - para "%1" - - - Prevent large data uploads when on a metered cellular connection - Evite cargas de grandes cantidades de datos cuando utilice una conexión celular medida - - - default - por defecto - - - metered - medido - - - unmetered - sin medidor - - - Wi-Fi Network Metered - Red Wi-Fi medida - - - Prevent large data uploads when on a metered Wi-Fi connection - Evite cargas de grandes cantidades de datos cuando esté en una conexión Wi-Fi medida - - - - 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 - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - OK - - - Cancel - Cancelar - - - - DeclinePage - - Back - Atrás - - - Decline, uninstall %1 - Rechazar, desinstalar %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - Modo de depuración de joystick - - - Longitudinal Maneuver Mode - Modo de maniobra longitudinal - - - openpilot Longitudinal Control (Alpha) - Control longitudinal de openpilot (fase experimental) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - AVISO: el control longitudinal de openpilot está en fase experimental para este automóvil y desactivará el Frenado Automático de Emergencia (AEB). - - - Enable ADB - Activar ADB - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB (Android Debug Bridge) permite conectar a su dispositivo por USB o por red. Visite https://docs.comma.ai/how-to/connect-to-comma para más información. - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - VER - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - Dongle ID - - - N/A - N/A - - - Serial - Serial - - - Pair Device - Emparejar Dispositivo - - - PAIR - EMPAREJAR - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Empareja tu dispositivo con comma connect (connect.comma.ai) y reclama tu oferta de comma prime. - - - Driver Camera - Cámara del conductor - - - PREVIEW - VISUALIZAR - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - Previsualizar la cámara del conductor para garantizar que la monitorización del sistema tenga buena visibilidad (el vehículo tiene que estar apagado) - - - Reset Calibration - Formatear Calibración - - - RESET - REINICIAR - - - Are you sure you want to reset calibration? - ¿Seguro que quiere formatear la calibración? - - - Reset - Formatear - - - Review Training Guide - Revisar la Guía de Entrenamiento - - - REVIEW - REVISAR - - - Are you sure you want to review the training guide? - ¿Seguro que quiere revisar la guía de entrenamiento? - - - Review - Revisar - - - Regulatory - Regulador - - - VIEW - VER - - - Change Language - Cambiar Idioma - - - CHANGE - CAMBIAR - - - Select a language - Seleccione el idioma - - - Reboot - Reiniciar - - - Power Off - Apagar - - - Your device is pointed %1° %2 and %3° %4. - Su dispositivo está apuntando %1° %2 y %3° %4. - - - down - abajo - - - up - arriba - - - left - izquierda - - - right - derecha - - - Are you sure you want to reboot? - ¿Seguro qué quiere reiniciar? - - - Disengage to Reboot - Desactivar para Reiniciar - - - Are you sure you want to power off? - ¿Seguro qué quiere apagar? - - - Disengage to Power Off - Desactivar para apagar - - - Disengage to Reset Calibration - Desactivar para restablecer la calibración - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - Openpilot se calibra continuamente, por lo que rara vez es necesario reiniciarlo. Restablecer la calibración reiniciará Openpilot si el automóvil está encendido. - - - - -Steering lag calibration is %1% complete. - - -La calibración del retraso de dirección está %1% completada. - - - - -Steering lag calibration is complete. - - -La calibración del retraso de la dirección está completa. - - - Steering torque response calibration is %1% complete. - La calibración de la respuesta del par de dirección está %1% completada. - - - Steering torque response calibration is complete. - La calibración de la respuesta del par de dirección está completa. - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - Regulador - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - ¿Seguro que quiere revisar la guía de entrenamiento? - - - Review - Revisar - - - Select a language - Seleccione el idioma - - - Wake-Up Behavior - - - - Reboot - Reiniciar - - - Power Off - Apagar - - - 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 - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - iniciando cámara - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - MODO EXPERIMENTAL - - - CHILL MODE ON - MODO CHILL - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - VERIFICAR - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - Modo Firehose: ACTIVO - - - ACTIVE - ACTIVO - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>%n segmento</b> de tu conducción está en el conjunto de datos de entrenamiento hasta ahora. - <b>%n segmentos</b> de tu conducción están en el conjunto de datos de entrenamiento hasta ahora. - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVO</span>: conéctate a una red sin límite de datos - - - Firehose Mode - Modo manguera contra incendios - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - MAX - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - MAX - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - Cancelar - - - Need at least %n character(s)! - - ¡Necesita mínimo %n caracter! - ¡Necesita mínimo %n caracteres! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - SELECCIONAR - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Formatear Calibración - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - Seleccionar - - - Cancel - Cancelar - - - - Networking - - Advanced - Avanzado - - - Enter password - Ingresar contraseña - - - for "%1" - para "%1" - - - Wrong password - Contraseña incorrecta - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - La temperatura del dispositivo es muy alta. El sistema se está enfriando antes de iniciar. Temperatura actual del componente interno: %1 - - - Unable to download updates -%1 - Incapaz de descargar actualizaciones. -%1 - - - Taking camera snapshots. System won't start until finished. - Tomando capturas de las cámaras. El sistema no se iniciará hasta que finalice. - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - Se está descargando una actualización del sistema operativo de su dispositivo en segundo plano. Se le pedirá que actualice cuando esté listo para instalarse. - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - - - - Acknowledge Excessive Actuation - - - - Snooze Update - Posponer Actualización - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - ACTUALIZAR - - - ALERTS - ALERTAS - - - ALERT - ALERTA - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - TOME CONTROL INMEDIATAMENTE - - - Reboot Device - Reiniciar Dispositivo - - - Waiting to start - Esperando para iniciar - - - System Unresponsive - Systema no responde - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - VERIFICAR - - - Country - - - - SELECT - SELECCIONAR - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - ACTUALIZAR - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - Empareje su dispositivo con su cuenta de comma - - - Go to https://connect.comma.ai on your phone - Vaya a https://connect.comma.ai en su teléfono - - - Click "add new device" and scan the QR code on the right - Seleccione "agregar nuevo dispositivo" y escanee el código QR a la derecha - - - Bookmark connect.comma.ai to your home screen to use it like an app - Añada connect.comma.ai a su pantalla de inicio para usarlo como una aplicación - - - Please connect to Wi-Fi to complete initial pairing - Conéctese a Wi-Fi para completar el emparejamiento inicial - - - - ParamControl - - Enable - Activar - - - Cancel - Cancelar - - - - ParamControlSP - - Enable - Activar - - - Cancel - Cancelar - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - ELIMINAR - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - Cancelar - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - Actualizar Ahora - - - Become a comma prime member at connect.comma.ai - Hazte miembro de comma prime en connect.comma.ai - - - PRIME FEATURES: - BENEFICIOS PRIME: - - - Remote access - Acceso remoto - - - 24/7 LTE connectivity - Conectividad LTE 24/7 - - - 1 year of drive storage - 1 año de almacenamiento - - - Remote snapshots - Capturas remotas - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ SUSCRITO - - - comma prime - comma prime - - - - QObject - - now - ahora - - - %n minute(s) ago - - hace %n min - hace %n mins - - - - %n hour(s) ago - - hace %n hora - hace %n horas - - - - %n day(s) ago - - hace %n día - hace %n días - - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - Dispositivo - - - Network - Red - - - Toggles - Ajustes - - - Software - Software - - - Developer - Desarrollador - - - Firehose - Firehose - - - - SettingsWindowSP - - × - × - - - Device - Dispositivo - - - Network - Red - - - sunnylink - - - - Toggles - Ajustes - - - Software - Software - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - Firehose - - - Developer - Desarrollador - - - Display - - - - - SetupWidget - - Finish Setup - Terminar configuración - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Empareje su dispositivo con comma connect (connect.comma.ai) y reclame su oferta de comma prime. - - - Pair device - Emparejar dispositivo - - - - Sidebar - - CONNECT - CONNECT - - - OFFLINE - OFFLINE - - - ONLINE - EN LÍNEA - - - ERROR - ERROR - - - TEMP - TEMP - - - HIGH - ALTA - - - GOOD - BUENA - - - OK - OK - - - VEHICLE - VEHÍCULO - - - NO - SIN - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - OFFLINE - - - REGIST... - - - - ONLINE - EN LÍNEA - - - ERROR - ERROR - - - SUNNYLINK - - - - - SoftwarePanel - - Updates are only downloaded while the car is off. - Actualizaciones solo se descargan con el auto apagado. - - - Current Version - Versión Actual - - - Download - Descargar - - - CHECK - VERIFICAR - - - Install Update - Actualizar - - - INSTALL - INSTALAR - - - Target Branch - Rama objetivo - - - SELECT - SELECCIONAR - - - Select a branch - Selecione una rama - - - Uninstall %1 - Desinstalar %1 - - - UNINSTALL - DESINSTALAR - - - Are you sure you want to uninstall? - ¿Seguro qué desea desinstalar? - - - Uninstall - Desinstalar - - - failed to check for update - no se pudo buscar actualizaciones - - - DOWNLOAD - DESCARGAR - - - update available - actualización disponible - - - never - nunca - - - up to date, last checked %1 - actualizado, último chequeo %1 - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - Selecione una rama - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - Reiniciar - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - Clave SSH - - - 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. - Aviso: Esto otorga acceso SSH a todas las claves públicas en su Github. Nunca ingrese un nombre de usuario de Github que no sea suyo. Un empleado de comma NUNCA le pedirá que añada un usuario de Github que no sea el suyo. - - - ADD - AÑADIR - - - Enter your GitHub username - Ingrese su usuario de GitHub - - - LOADING - CARGANDO - - - REMOVE - ELIMINAR - - - Username '%1' has no keys on GitHub - El usuario "%1” no tiene claves en GitHub - - - Request timed out - Solicitud expirada - - - Username '%1' doesn't exist on GitHub - El usuario '%1' no existe en Github - - - - SshToggle - - Enable SSH - 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 - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - EMPAREJAR - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - N/A - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - Rechazar - - - Agree - Aceptar - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Experimental Mode - Modo Experimental - - - Disengage on Accelerator Pedal - Desactivar con el Acelerador - - - Enable Lane Departure Warnings - Activar Avisos de Salida de Carril - - - 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). - Recibir alertas para volver al carril cuando su vehículo se salga fuera del carril sin que esté activada la señal de giro y esté conduciendo por encima de 50 km/h (31 mph). - - - Always-On Driver Monitoring - Monitoreo Permanente del Conductor - - - Record and Upload Driver Camera - Grabar y Subir Cámara del Conductor - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - Subir datos de la cámara del conductor para ayudar a mejorar el algoritmo de monitoreo del conductor. - - - Use Metric System - Usar Sistema Métrico - - - Display speed in km/h instead of mph. - Mostrar velocidad en km/h en vez de mph. - - - Aggressive - Agresivo - - - Standard - Estándar - - - Relaxed - Relajado - - - Driving Personality - Personalidad de conducción - - - End-to-End Longitudinal Control - Control Longitudinal de Punta a Punta - - - New Driving Visualization - Nueva Visualización de la conducción - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - La visualización de la conducción cambiará a la cámara que enfoca la carretera a velocidades bajas para mostrar mejor los giros. El logo del modo experimental también se mostrará en la esquina superior derecha. - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 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. - - - Changing this setting will restart openpilot if the car is powered on. - Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. - - - Record and Upload Microphone Audio - Grabar y cargar audio de micrófono - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - Graba y almacena el audio del micrófono mientras conduces. El audio se incluirá en el video de la cámara del tablero en comma connect. - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - Seleccionar - - - Cancel - Cancelar - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - 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> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - Buscando redes... - - - CONNECTING... - CONECTANDO... - - - FORGET - OLVIDAR - - - Forget Wi-Fi Network "%1"? - ¿Olvidar la Red de Wi-Fi "%1"? - - - Forget - Olvidar - - - diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts deleted file mode 100644 index c6c998b723..0000000000 --- a/selfdrive/ui/translations/main_fr.ts +++ /dev/null @@ -1,2849 +0,0 @@ - - - - - AbstractAlert - - Close - Fermer - - - Reboot and Update - Redémarrer et mettre à jour - - - - AdvancedNetworking - - Back - Retour - - - Enable Tethering - Activer le partage de connexion - - - Tethering Password - Mot de passe du partage de connexion - - - EDIT - MODIFIER - - - Enter new tethering password - Entrez le nouveau mot de passe du partage de connexion - - - IP Address - Adresse IP - - - Enable Roaming - Activer l'itinérance - - - APN Setting - Paramètre APN - - - Enter APN - Entrer le nom du point d'accès - - - leave blank for automatic configuration - laisser vide pour une configuration automatique - - - Cellular Metered - Connexion cellulaire limitée - - - Hidden Network - Réseau Caché - - - CONNECT - CONNECTER - - - Enter SSID - Entrer le SSID - - - Enter password - Entrer le mot de passe - - - for "%1" - pour "%1" - - - Prevent large data uploads when on a metered cellular connection - - - - default - - - - metered - - - - unmetered - - - - Wi-Fi Network Metered - - - - Prevent large data uploads when on a metered Wi-Fi connection - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - Ok - - - Cancel - Annuler - - - - DeclinePage - - Back - Retour - - - Decline, uninstall %1 - Refuser, désinstaller %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - Mode débogage au joystick - - - Longitudinal Maneuver Mode - Mode manœuvre longitudinale - - - openpilot Longitudinal Control (Alpha) - Contrôle longitudinal openpilot (Alpha) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - ATTENTION : le contrôle longitudinal openpilot est en alpha pour cette voiture et désactivera le freinage d'urgence automatique (AEB). - - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - VOIR - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - Dongle ID - - - N/A - N/A - - - Serial - N° de série - - - Driver Camera - Caméra conducteur - - - PREVIEW - APERÇU - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - Aperçu de la caméra orientée vers le conducteur pour assurer une bonne visibilité de la surveillance du conducteur. (véhicule doit être éteint) - - - Reset Calibration - Réinitialiser la calibration - - - RESET - RÉINITIALISER - - - Are you sure you want to reset calibration? - Êtes-vous sûr de vouloir réinitialiser la calibration ? - - - Reset - Réinitialiser - - - Review Training Guide - Revoir le guide de formation - - - REVIEW - REVOIR - - - Are you sure you want to review the training guide? - Êtes-vous sûr de vouloir revoir le guide de formation ? - - - Review - Revoir - - - Regulatory - Réglementaire - - - VIEW - VOIR - - - Change Language - Changer de langue - - - CHANGE - CHANGER - - - Select a language - Choisir une langue - - - Reboot - Redémarrer - - - Power Off - Éteindre - - - Your device is pointed %1° %2 and %3° %4. - Votre appareil est orienté %1° %2 et %3° %4. - - - down - bas - - - up - haut - - - left - gauche - - - right - droite - - - Are you sure you want to reboot? - Êtes-vous sûr de vouloir redémarrer ? - - - Disengage to Reboot - Désengager pour redémarrer - - - Are you sure you want to power off? - Êtes-vous sûr de vouloir éteindre ? - - - Disengage to Power Off - Désengager pour éteindre - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Associez votre appareil avec comma connect (connect.comma.ai) et profitez de l'offre comma prime. - - - Pair Device - Associer l'appareil - - - PAIR - ASSOCIER - - - Disengage to Reset Calibration - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - Réglementaire - - - Language - - - - Reset Settings - - - - 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 - - - Wake-Up Behavior - - - - Reboot - Redémarrer - - - Power Off - Éteindre - - - 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 - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - démarrage de la caméra - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - MODE EXPÉRIMENTAL ACTIVÉ - - - CHILL MODE ON - MODE DÉTENTE ACTIVÉ - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - VÉRIFIER - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - - - - ACTIVE - - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - - - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode - - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mi/h - - - MAX - MAX - - - - HudRendererSP - - km/h - km/h - - - mph - mi/h - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - MAX - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - Annuler - - - Need at least %n character(s)! - - Besoin d'au moins %n caractère ! - Besoin d'au moins %n caractères ! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - SÉLECTIONNER - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Réinitialiser la calibration - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - Continuer - - - on Metered - - - - Cancel - Annuler - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - Sélectionner - - - Cancel - Annuler - - - - Networking - - Advanced - Avancé - - - Enter password - Entrer le mot de passe - - - for "%1" - pour "%1" - - - Wrong password - Mot de passe incorrect - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - Température de l'appareil trop élevée. Le système doit refroidir avant de démarrer. Température actuelle de l'appareil : %1 - - - Unable to download updates -%1 - Impossible de télécharger les mises à jour -%1 - - - Taking camera snapshots. System won't start until finished. - Capture de clichés photo. Le système ne démarrera pas tant qu'il n'est pas terminé. - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - Une mise à jour du système d'exploitation de votre appareil est en cours de téléchargement en arrière-plan. Vous serez invité à effectuer la mise à jour lorsqu'elle sera prête à être installée. - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - - - - Acknowledge Excessive Actuation - - - - Snooze Update - Reporter la mise à jour - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - MISE À JOUR - - - ALERTS - ALERTES - - - ALERT - ALERTE - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - REPRENEZ LE CONTRÔLE IMMÉDIATEMENT - - - Reboot Device - Redémarrer l'appareil - - - Waiting to start - En attente de démarrage - - - System Unresponsive - Système inopérant - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - VÉRIFIER - - - Country - - - - SELECT - SÉLECTIONNER - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - MISE À JOUR - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - Associez votre appareil à votre compte comma - - - Go to https://connect.comma.ai on your phone - Allez sur https://connect.comma.ai sur votre téléphone - - - Click "add new device" and scan the QR code on the right - Cliquez sur "ajouter un nouvel appareil" et scannez le code QR à droite - - - Bookmark connect.comma.ai to your home screen to use it like an app - Ajoutez connect.comma.ai à votre écran d'accueil pour l'utiliser comme une application - - - Please connect to Wi-Fi to complete initial pairing - Connectez-vous au Wi-Fi pour terminer l'appairage initial - - - - ParamControl - - Enable - Activer - - - Cancel - Annuler - - - - ParamControlSP - - Enable - Activer - - - Cancel - Annuler - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - SUPPRIMER - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - Annuler - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - Mettre à niveau - - - Become a comma prime member at connect.comma.ai - Devenez membre comma prime sur connect.comma.ai - - - PRIME FEATURES: - FONCTIONNALITÉS PRIME : - - - Remote access - Accès à distance - - - 24/7 LTE connectivity - Connexion LTE 24/7 - - - 1 year of drive storage - 1 an de stockage de trajets - - - Remote snapshots - Captures à distance - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ ABONNÉ - - - comma prime - comma prime - - - - QObject - - %n minute(s) ago - - il y a %n minute - il y a %n minutes - - - - %n hour(s) ago - - il y a %n heure - il y a %n heures - - - - %n day(s) ago - - il y a %n jour - il y a %n jours - - - - now - maintenant - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - Appareil - - - Network - Réseau - - - Toggles - Options - - - Software - Logiciel - - - Developer - Dév. - - - Firehose - - - - - SettingsWindowSP - - × - × - - - Device - Appareil - - - Network - Réseau - - - sunnylink - - - - Toggles - Options - - - Software - Logiciel - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - - - - Developer - Dév. - - - Display - - - - - SetupWidget - - Finish Setup - Terminer l'installation - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Associez votre appareil avec comma connect (connect.comma.ai) et profitez de l'offre comma prime. - - - Pair device - Associer l'appareil - - - - Sidebar - - CONNECT - CONNECTER - - - OFFLINE - HORS LIGNE - - - ONLINE - EN LIGNE - - - ERROR - ERREUR - - - TEMP - TEMP - - - HIGH - HAUT - - - GOOD - BON - - - OK - OK - - - VEHICLE - VÉHICULE - - - NO - NON - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - HORS LIGNE - - - REGIST... - - - - ONLINE - EN LIGNE - - - ERROR - ERREUR - - - SUNNYLINK - - - - - SoftwarePanel - - Updates are only downloaded while the car is off. - Les MàJ sont téléchargées uniquement si la voiture est éteinte. - - - Current Version - Version actuelle - - - Download - Télécharger - - - CHECK - VÉRIFIER - - - Install Update - Installer la mise à jour - - - INSTALL - INSTALLER - - - Target Branch - Branche cible - - - SELECT - SÉLECTIONNER - - - Select a branch - Sélectionner une branche - - - Uninstall %1 - Désinstaller %1 - - - UNINSTALL - DÉSINSTALLER - - - Are you sure you want to uninstall? - Êtes-vous sûr de vouloir désinstaller ? - - - Uninstall - Désinstaller - - - failed to check for update - échec de la vérification de la mise à jour - - - DOWNLOAD - TÉLÉCHARGER - - - update available - mise à jour disponible - - - never - jamais - - - up to date, last checked %1 - à jour, dernière vérification %1 - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - Sélectionner une branche - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - Redémarrer - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - Retour - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - Retour - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - Clés SSH - - - 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. - Attention : Ceci accorde l'accès SSH à toutes les clés publiques de vos paramètres GitHub. N'entrez jamais un nom d'utilisateur GitHub autre que le vôtre. Un employé de comma ne vous demandera JAMAIS d'ajouter son nom d'utilisateur GitHub. - - - ADD - AJOUTER - - - Enter your GitHub username - Entrez votre nom d'utilisateur GitHub - - - LOADING - CHARGEMENT - - - REMOVE - SUPPRIMER - - - Username '%1' has no keys on GitHub - L'utilisateur '%1' n'a pas de clés sur GitHub - - - Request timed out - Délai de la demande dépassé - - - Username '%1' doesn't exist on GitHub - L'utilisateur '%1' n'existe pas sur GitHub - - - - SshToggle - - Enable SSH - 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 - - - - 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. - - - - N/A - N/A - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - Refuser - - - Agree - Accepter - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Experimental Mode - Mode expérimental - - - Disengage on Accelerator Pedal - Désengager avec la pédale d'accélérateur - - - Enable Lane Departure Warnings - Activer les avertissements de sortie de voie - - - 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). - Recevez des alertes pour revenir dans la voie lorsque votre véhicule dérive au-delà d'une ligne de voie détectée sans clignotant activé en roulant à plus de 31 mph (50 km/h). - - - Record and Upload Driver Camera - Enregistrer et télécharger la caméra conducteur - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - Publiez les données de la caméra orientée vers le conducteur et aidez à améliorer l'algorithme de surveillance du conducteur. - - - Use Metric System - Utiliser le système métrique - - - Display speed in km/h instead of mph. - Afficher la vitesse en km/h au lieu de mph. - - - Aggressive - Aggressif - - - Standard - Standard - - - Relaxed - Détendu - - - Driving Personality - Personnalité de conduite - - - New Driving Visualization - Nouvelle visualisation de la conduite - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 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. - - - End-to-End Longitudinal Control - Contrôle longitudinal de bout en bout - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - La visualisation de la conduite passera sur la caméra grand angle dirigée vers la route à faible vitesse afin de mieux montrer certains virages. Le logo du mode expérimental s'affichera également dans le coin supérieur droit. - - - Always-On Driver Monitoring - Surveillance continue du conducteur - - - Changing this setting will restart openpilot if the car is powered on. - - - - Record and Upload Microphone Audio - - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - Sélectionner - - - Cancel - Annuler - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - WiFiPromptWidget - - 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> - - - - - WifiUI - - Scanning for networks... - Recherche de réseaux... - - - CONNECTING... - CONNEXION... - - - FORGET - OUBLIER - - - Forget Wi-Fi Network "%1"? - Oublier le réseau Wi-Fi "%1" ? - - - Forget - Oublier - - - diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts deleted file mode 100644 index b6a29a867c..0000000000 --- a/selfdrive/ui/translations/main_ja.ts +++ /dev/null @@ -1,2848 +0,0 @@ - - - - - AbstractAlert - - Close - 閉じる - - - Reboot and Update - 再起動してアップデート - - - - AdvancedNetworking - - Back - 戻る - - - Enable Tethering - テザリング有効 - - - Tethering Password - テザリングパスワード - - - EDIT - 編集 - - - Enter new tethering password - 新しいテザリングパスワードを入力 - - - IP Address - IPアドレス - - - Enable Roaming - ローミング有効 - - - APN Setting - APN設定 - - - Enter APN - APNを入力 - - - leave blank for automatic configuration - 自動で設定するには空白のままにしてください - - - Cellular Metered - 従量制通信設定 - - - Hidden Network - ネットワーク非表示 - - - CONNECT - 接続 - - - Enter SSID - SSIDを入力 - - - Enter password - パスワードを入力 - - - for "%1" - [%1] - - - Prevent large data uploads when on a metered cellular connection - モバイルデータ回線を使用しているときは大容量データをアップロードしません - - - default - 標準設定 - - - metered - 従量制 - - - unmetered - 定額制 - - - Wi-Fi Network Metered - 従量制のWi-Fiネットワーク - - - Prevent large data uploads when on a metered Wi-Fi connection - 通信制限のあるWi-Fi接続では大容量データをアップロードしません - - - - 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 - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - OK - - - Cancel - キャンセル - - - - DeclinePage - - Back - 戻る - - - Decline, uninstall %1 - 同意しない(%1をアンインストール) - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - ジョイスティックデバッグモード - - - Longitudinal Maneuver Mode - アクセル制御マニューバー - - - openpilot Longitudinal Control (Alpha) - openpilotアクセル制御(Alpha) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - この車ではopenpilotのアクセル制御はアルファ版であり、自動緊急ブレーキ(AEB)が無効化されます。 - - - Enable ADB - ADBを有効にする - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB(Android Debug Bridge)により、USBまたはネットワーク経由でデバイスに接続できます。詳細は、https://docs.comma.ai/how-to/connect-to-comma を参照してください。 - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - 確認 - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - ドングルID - - - N/A - 該当なし - - - Serial - シリアル番号 - - - Driver Camera - 車内カメラ - - - PREVIEW - プレビュー - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - 車内カメラでドライバー監視システムのカメラ画像を確認できます。(車両のパワーOFF時の機能です) - - - Reset Calibration - キャリブレーションリセット - - - RESET - リセット - - - Are you sure you want to reset calibration? - キャリブレーションをリセットしますか? - - - Review Training Guide - トレーニングガイドを見る - - - REVIEW - 確認 - - - Are you sure you want to review the training guide? - トレーニングガイドを始めてもよろしいですか? - - - Regulatory - 規約 - - - VIEW - 確認 - - - Change Language - 多言語対応 - - - CHANGE - 変更 - - - Select a language - 言語を選択 - - - Reboot - 再起動 - - - Power Off - パワーオフ - - - Your device is pointed %1° %2 and %3° %4. - このデバイスは%2 %1°、%4 %3°の向きに設置されています。 - - - down - - - - up - - - - left - - - - right - - - - Are you sure you want to reboot? - 再起動してもよろしいですか? - - - Disengage to Reboot - 再起動するには車を一旦停止してください - - - Are you sure you want to power off? - パワーオフしてもよろしいですか? - - - Disengage to Power Off - パワーオフするには車を一旦停止してください - - - Reset - リセット - - - Review - 見る - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - デバイスをcommaコネクト(connect.comma.ai)でペアリングしてcommaプライムの特典を受け取ってください。 - - - Pair Device - デバイスのペアリング - - - PAIR - OK - - - Disengage to Reset Calibration - キャリブレーションをリセットするには運転支援を解除して下さい。 - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - openpilot は継続的にキャリブレーションを行っており、リセットが必要になることはほとんどありません。キャリブレーションをリセットすると、車の電源が入っている場合はopenpilotが再起動します。 - - - - -Steering lag calibration is %1% complete. - - -ステアリング遅延のキャリブレーションが%1%完了。 - - - - -Steering lag calibration is complete. - - -ステアリング遅延のキャリブレーション完了。 - - - Steering torque response calibration is %1% complete. - ステアリングトルク応答のキャリブレーションが%1%完了。 - - - Steering torque response calibration is complete. - ステアリングトルク応答のキャリブレーション完了。 - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - 規約 - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - トレーニングガイドを始めてもよろしいですか? - - - Review - 見る - - - Select a language - 言語を選択 - - - Wake-Up Behavior - - - - Reboot - 再起動 - - - Power Off - パワーオフ - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - Are you sure you want to enter Always Offroad mode? - - - - Disengage to Enter Always Offroad Mode - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - カメラ起動中 - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - EXPERIMENTALモード - - - CHILL MODE ON - CHILLモード - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - 確認 - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - Firehoseモード: 作動中 - - - ACTIVE - 動作中 - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - あなたの運転の<b>%nセグメント</b>がこれまでのトレーニングデータに含まれています。 - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>動作停止</span>: 大容量のネットワークに接続してください - - - Firehose Mode - Firehoseモード - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - 最大速度 - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - 最大速度 - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - キャンセル - - - Need at least %n character(s)! - - %n文字以上にして下さい! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - 選択 - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - キャリブレーションリセット - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - 続ける - - - on Metered - - - - Cancel - キャンセル - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - 選択 - - - Cancel - キャンセル - - - - Networking - - Advanced - 詳細 - - - Enter password - パスワードを入力 - - - for "%1" - [%1] - - - Wrong password - パスワードが違います - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Unable to download updates -%1 - 更新をダウンロードできませんでした -%1 - - - Taking camera snapshots. System won't start until finished. - スナップショットを撮影中です。完了するまでシステムは起動しません。 - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - オペレーティングシステムがバックグラウンドでダウンロードされています。インストールの準備が整うと更新を促されます。 - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - デバイスの温度が高すぎるためシステム起動前の冷却中です。現在のデバイス内部温度: %1 - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - 製品のcomma.aiへの登録に失敗しました。このデバイスはcomma.aiのサーバーに接続したりアップロードを行ったりすることはできず、comma.aiからのサポートも受けられません。もしcomma.ai/shopで購入した場合は、https://comma.ai/support にてサポートチケットをご提出ください。 - - - Acknowledge Excessive Actuation - 過剰な作動の検知 - - - Snooze Update - また後で更新する - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - 更新 - - - ALERTS - 警告 - - - ALERT - 警告 - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - 直ちに車の運転に戻って下さい - - - Reboot Device - デバイスを再起動してください - - - Waiting to start - 始動を待機しています - - - System Unresponsive - システムが応答しません - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - 確認 - - - Country - - - - SELECT - 選択 - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - 更新 - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - デバイスとcommaアカウントを連携して下さい - - - Go to https://connect.comma.ai on your phone - スマートフォンで https://connect.comma.ai にアクセスしてください - - - Click "add new device" and scan the QR code on the right - 「add new device」を押して右側のQRコードをスキャンしてください - - - Bookmark connect.comma.ai to your home screen to use it like an app - connect.comma.aiのサイトをホーム画面に追加して、アプリのように使うことができます。 - - - Please connect to Wi-Fi to complete initial pairing - 最初にペアリングするため、Wi-Fiに接続してください - - - - ParamControl - - Cancel - キャンセル - - - Enable - 有効にする - - - - ParamControlSP - - Enable - 有効にする - - - Cancel - キャンセル - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - 削除 - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - キャンセル - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - 今すぐアップグレード - - - Become a comma prime member at connect.comma.ai - connect.comma.ai からプライム会員に登録できます - - - PRIME FEATURES: - 特典: - - - Remote access - リモートアクセス - - - 24/7 LTE connectivity - 24時間365日のLTE接続 - - - 1 year of drive storage - 1年間分のドライブストレージ - - - Remote snapshots - リモートスナップショット - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ 有効です - - - comma prime - commaプライム - - - - QObject - - %n minute(s) ago - - %n分前 - - - - %n hour(s) ago - - %n時間前 - - - - %n day(s) ago - - %n日前 - - - - now - たった今 - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - デバイス - - - Network - ネット - - - Toggles - 機能 - - - Software - ソフト - - - Developer - 開発 - - - Firehose - データ学習 - - - - SettingsWindowSP - - × - × - - - Device - デバイス - - - Network - ネット - - - sunnylink - - - - Toggles - 機能 - - - Software - ソフト - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - データ学習 - - - Developer - 開発 - - - Display - - - - - SetupWidget - - Finish Setup - セットアップの完了 - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - デバイスをcommaコネクト(connect.comma.ai)でペアリングしてcommaプライムの特典を受け取ってください。 - - - Pair device - デバイスのペアリング - - - - Sidebar - - CONNECT - 接続 - - - OFFLINE - オフライン - - - ONLINE - オンライン - - - ERROR - エラー - - - TEMP - 温度 - - - HIGH - 高温 - - - GOOD - 最適 - - - OK - OK - - - VEHICLE - 車両 - - - NO - NO - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - オフライン - - - REGIST... - - - - ONLINE - オンライン - - - ERROR - エラー - - - SUNNYLINK - - - - - SoftwarePanel - - Updates are only downloaded while the car is off. - 車の電源がオフの間のみアップデートがダウンロードできます。 - - - Current Version - 現在のバージョン - - - Download - ダウンロード - - - Install Update - アップデート - - - INSTALL - インストール - - - Target Branch - 対象のブランチ - - - SELECT - 選択 - - - Select a branch - ブランチを選択 - - - UNINSTALL - 実行 - - - Uninstall %1 - %1をアンインストール - - - Are you sure you want to uninstall? - アンインストールしてもよろしいですか? - - - CHECK - 確認 - - - Uninstall - アンインストール - - - failed to check for update - アップデートの確認に失敗しました。 - - - up to date, last checked %1 - 最新の状態です。最終確認日時:%1 - - - DOWNLOAD - ダウンロード - - - update available - アップデートが利用可能です - - - never - 無効 - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - ブランチを選択 - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - 再起動 - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - 戻る - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - 戻る - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - SSH 鍵 - - - 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のユーザー名を追加するようお願いすることはありません。 - - - ADD - 追加 - - - Enter your GitHub username - GitHubのユーザー名を入力してください - - - LOADING - 読み込み中 - - - REMOVE - 削除 - - - Username '%1' has no keys on GitHub - ユーザー名“%1”は GitHub に公開鍵がありません - - - Request timed out - リクエストタイムアウト - - - Username '%1' doesn't exist on GitHub - ユーザー名”%1”は GitHub に存在しません - - - - SshToggle - - Enable SSH - SSHの有効化 - - - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - Enable sunnylink - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - OK - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - 該当なし - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - 拒否 - - - Agree - 同意 - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - 車線逸脱警報の有効化 - - - Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). - 時速31マイル(50km)以上のスピードで走行中、ウインカーを作動させずに検出したレーン上に車両が触れた場合、手動で車線内に戻るように警告を行います。 - - - Use Metric System - メートル法の使用 - - - Display speed in km/h instead of mph. - 速度は mph ではなく km/h で表示されます。 - - - Record and Upload Driver Camera - 車内カメラの録画とアップロード - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - 車内カメラの映像をアップロードし、ドライバー監視システムのアルゴリズムの向上に役立てます。 - - - Disengage on Accelerator Pedal - アクセルを踏むと運転サポートを中断 - - - Experimental Mode - Experimentalモード - - - New Driving Visualization - 新しい運転画面 - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 車両の標準ACC(アダプティブ・クルーズ・コントロール)がアクセル制御に使用されているため、現在Experimentalモードは利用できません。 - - - Aggressive - アグレッシブ - - - Standard - 標準 - - - Relaxed - リラックス - - - Driving Personality - 運転傾向 - - - End-to-End Longitudinal Control - End-to-Endアクセル制御 - - - openpilot longitudinal control may come in a future update. - openpilotのアクセル制御は将来のアップデートで利用できる可能性があります。 - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - 運転時の画面効果として、低速時にカーブをより良く表示するために道路用の広角カメラに切り替わります。またExperimentalモードのロゴが右上隅に表示されます。 - - - Always-On Driver Monitoring - 運転者の常時モニタリング - - - Changing this setting will restart openpilot if the car is powered on. - この設定を変更すると車の電源が入っている場合はopenpilotが再起動します。 - - - Record and Upload Microphone Audio - マイク音声の録音とアップロード - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - 運転中にマイク音声を録音・保存します。音声は comma connect のドライブレコーダー映像に含まれます。 - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - 選択 - - - Cancel - キャンセル - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - この設定を変更すると車の電源が入っている場合はopenpilotが再起動します。 - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - 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モード <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - ネットワークをスキャン中... - - - CONNECTING... - 接続中... - - - FORGET - 削除 - - - Forget Wi-Fi Network "%1"? - Wi-Fiネットワーク%1を削除してもよろしいですか? - - - Forget - 削除 - - - diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts deleted file mode 100644 index 164ce09f07..0000000000 --- a/selfdrive/ui/translations/main_ko.ts +++ /dev/null @@ -1,2867 +0,0 @@ - - - - - AbstractAlert - - Close - 닫기 - - - Reboot and Update - 업데이트 및 재부팅 - - - - AdvancedNetworking - - Back - 뒤로 - - - Enable Tethering - 테더링 사용 - - - Tethering Password - 테더링 비밀번호 - - - EDIT - 편집 - - - Enter new tethering password - 새 테더링 비밀번호 입력 - - - IP Address - IP 주소 - - - Enable Roaming - 로밍 사용 - - - APN Setting - APN 설정 - - - Enter APN - APN 입력 - - - leave blank for automatic configuration - 자동 설정하려면 비워 두세요 - - - Cellular Metered - 셀룰러 종량제 - - - Hidden Network - 숨겨진 네트워크 - - - CONNECT - 연결 - - - Enter SSID - SSID 입력 - - - Enter password - 비밀번호 입력 - - - for "%1" - "%1"의 비밀번호를 입력하세요 - - - Prevent large data uploads when on a metered cellular connection - 데이터 사용량 제한이 있는 셀룰러 연결 시 대용량 데이터 업로드 방지 - - - default - 기본 - - - metered - 종량제 - - - unmetered - 무제한 - - - Wi-Fi Network Metered - Wi-Fi 네트워크 종량제 - - - Prevent large data uploads when on a metered Wi-Fi connection - 데이터 사용량 제한이 있는 Wi-Fi 연결 시 대용량 데이터 업로드 방지 - - - - 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 - 조작 필요 없음 - - - - Brightness - - Auto (Dark) - 자동 (어둡게) - - - Auto - 자동 - - - Global Brightness - 전역 밝기 - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - 기기의 밝기를 강제로 설정합니다. 이 설정은 주행 중 화면과 비주행 화면(설정 등) 모두에 적용됩니다. - - - - ConfirmationDialog - - Ok - 확인 - - - Cancel - 취소 - - - - DeclinePage - - Back - 뒤로 - - - Decline, uninstall %1 - 거절, %1 제거 - - - You must accept the Terms and Conditions in order to use sunnypilot. - sunnypilot을 사용하려면 이용 약관에 동의해야 합니다. - - - - DeveloperPanel - - Joystick Debug Mode - 조이스틱 디버그 모드 - - - Longitudinal Maneuver Mode - 가감속 제어 모드 - - - openpilot Longitudinal Control (Alpha) - openpilot 가감속 제어 (알파) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 경고: 이 차량에서 openpilot 가감속 제어는 알파 단계이며 자동 긴급 제동(AEB)을 비활성화합니다. - - - Enable ADB - ADB 사용 - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB(Android Debug Bridge)는 USB 또는 네트워크를 통해 기기에 연결할 수 있게 해줍니다. 자세한 내용은 https://docs.comma.ai/how-to/connect-to-comma 를 참고하세요. - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - 이 차량에서는 openpilot의 가감속 제어 대신 sunnypilot이 차량의 내장 ACC를 기본값으로 사용합니다. -이를 활성화하면 openpilot 가감속 제어로 전환합니다. -openpilot 가감속 제어(알파)를 활성화할 때는 실험적 모드 활성화를 권장합니다. - - - - DeveloperPanelSP - - Show Advanced Controls - 고급 설정 표시 - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - sunnypilot 고급 설정의 표시 여부를 전환합니다. -이 설정은 설정의 표시 여부만 전환하며, 실제 기능의 켜짐/꺼짐 상태는 바꾸지 않습니다. - - - Enable GitHub runner service - GitHub runner 서비스 사용 - - - Enables or disables the github runner service. - GitHub Runner 서비스를 켜거나 끕니다. - - - Enable Quickboot Mode - 빠른 부팅 모드 사용 - - - Error Log - 오류 로그 - - - VIEW - 보기 - - - View the error log for sunnypilot crashes. - sunnypilot 충돌에 대한 오류 로그를 확인하세요. - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - 토글을 켜면 부팅 시간을 단축하기 위해 미리 빌드된 파일을 생성합니다. 토글을 끄면 로컬에서 편집한 cpp 파일을 컴파일할 수 있도록 해당 미리 빌드된 파일을 즉시 제거합니다. <br><br><b>기기에서 C++ 파일을 로컬로 편집하려면, 변경 사항이 다시 컴파일될 수 있도록 반드시 이 토글을 먼저 꺼야 합니다.</b> - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - 빠른 부팅 모드는 업데이트가 비활성화되어 있어야 합니다.<br>먼저 소프트웨어 패널에서 '업데이트 비활성화'를 활성화하세요. - - - Enable Copyparty service - Copyparty 서비스 사용 - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - Copyparty는 매우 유능한 파일 서버로, 이를 통해 주행 기록을 다운로드하고, 로그 파일을 확인하며, 심지어 브라우저에서 일부 파일을 편집할 수도 있습니다. 이 기능을 사용하려면 comma 기기의 로컬 IP를 통해 접속해야 합니다. - - - - DevicePanel - - Dongle ID - 동글 ID - - - N/A - N/A - - - Serial - 일련번호 - - - Driver Camera - 운전자 카메라 - - - PREVIEW - 미리보기 - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - 운전자 모니터링이 잘 보이는지 확인하기 위해 운전자 방향 카메라를 미리 확인하세요. (차량은 시동이 꺼져 있어야 합니다) - - - Reset Calibration - 캘리브레이션 - - - RESET - 초기화 - - - Are you sure you want to reset calibration? - 캘리브레이션을 초기화하시겠습니까? - - - Review Training Guide - 트레이닝 가이드 - - - REVIEW - 다시보기 - - - Are you sure you want to review the training guide? - 트레이닝 가이드를 다시 확인하시겠습니까? - - - Regulatory - 규제 - - - VIEW - 보기 - - - Change Language - 언어 변경 - - - CHANGE - 변경 - - - Select a language - 언어를 선택하세요 - - - Reboot - 재부팅 - - - Power Off - 전원 끄기 - - - Your device is pointed %1° %2 and %3° %4. - 사용자의 기기는 %2 %1° 및 %4 %3° 의 방향으로 장착되어 있습니다. - - - down - 아래로 - - - up - 위로 - - - left - 좌측으로 - - - right - 우측으로 - - - Are you sure you want to reboot? - 재부팅하시겠습니까? - - - Disengage to Reboot - 재부팅하려면 연결을 해제하세요 - - - Are you sure you want to power off? - 전원을 끄시겠습니까? - - - Disengage to Power Off - 전원을 끄려면 연결을 해제하세요 - - - Reset - 초기화 - - - Review - 다시보기 - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 기기를 comma connect (connect.comma.ai)와 페어링하고 comma prime 혜택을 받으세요. - - - Pair Device - 기기 페어링 - - - PAIR - 페어링 - - - Disengage to Reset Calibration - 캘리브레이션을 초기화하려면 해제하세요 - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - openpilot은 지속적으로 캘리브레이션을 수행하며, 캘리브레이션 초기화가 필요한 경우는 드뭅니다. 차량 전원이 켜져 있는 상태에서 캘리브레이션을 초기화하면 openpilot이 재시작됩니다. - - - - -Steering lag calibration is %1% complete. - - -조향 지연 캘리브레이션이 %1% 완료되었습니다. - - - - -Steering lag calibration is complete. - - -조향 지연 캘리브레이션이 완료되었습니다. - - - Steering torque response calibration is %1% complete. - 조향 토크 응답 캘리브레이션이 %1% 완료되었습니다. - - - Steering torque response calibration is complete. - 조향 토크 응답 캘리브레이션이 완료되었습니다. - - - Review the rules, features, and limitations of sunnypilot - sunnypilot의 규칙, 기능 및 제한 사항을 검토하세요. - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - sunnypilot은 기기를 좌우 4° 이내, 위로는 5° 이내, 아래로는 9° 이내로 장착해야 합니다. - - - - DevicePanelSP - - Quiet Mode - 정숙 모드 - - - Driver Camera Preview - 운전자 카메라 미리보기 - - - Training Guide - 교육 가이드 - - - Regulatory - 규제 - - - Language - 언어 - - - Reset Settings - 설정 초기화 - - - Are you sure you want to review the training guide? - 교육 가이드를 검토하시겠습니까? - - - Review - 검토 - - - Select a language - 언어를 선택하세요 - - - Wake-Up Behavior - 깨우기 동작 - - - Reboot - 재부팅 - - - Power Off - 전원 끄기 - - - Offroad Mode - 오프로드 모드 - - - Are you sure you want to exit Always Offroad mode? - 항상 오프로드 모드를 종료하시겠습니까? - - - Confirm - 확인 - - - Are you sure you want to enter Always Offroad mode? - 항상 오프로드 모드로 진입하시겠습니까? - - - Disengage to Enter Always Offroad Mode - 항상 오프로드 모드로 진입하려면 해제하세요 - - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. - 모든 Sunnypilot 설정을 기본값으로 재설정하시겠습니까? 설정을 재설정하면 되돌릴 수 없습니다. - - - Reset - 재설정 - - - The reset cannot be undone. You have been warned. - 재설정은 되돌릴 수 없습니다. 경고 드립니다. - - - Exit Always Offroad - 항상 오프로드 종료 - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - ⁍ 기본값: 기기가 정상적으로 부팅/절전 모드에서 해제되며, 작동 준비가 완료됩니다. - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - ⁍ 오프로드: 부팅/절전 해제 후 기기가 항상 오프로드 모드로 유지됩니다. - - - Controls state of the device after boot/sleep. - 부팅/절전 모드 후 기기의 상태를 제어합니다. - - - Onroad Uploads - 주행 중 업로드 - - - Enable Always Offroad - 항상 오프로드 사용 - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - 주행 중 화면: 밝기 감소 - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - 운전이 시작된 후 기기 화면을 끄거나 밝기를 줄입니다. 화면을 터치하거나 시각적 알림이 표시되면 자동으로 다시 밝아집니다. - - - Interactivity Timeout - 상호작용 타임아웃 - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - 설정 UI에 사용자 지정 타임아웃을 적용하세요. -이는 사용자가 화면과 상호 작용하지 않으면 설정 UI가 자동으로 닫히는 시간입니다. - - - - DriveStats - - Drives - 주행 - - - Hours - 시간 - - - ALL TIME - 전체 기간 - - - PAST WEEK - 지난주 - - - KM - KM - - - Miles - mi - - - - DriverViewWindow - - camera starting - 카메라 시작 중 - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - 항상 오프로드 모드를 종료하시겠습니까? - - - Confirm - 확인 - - - EXIT ALWAYS OFFROAD MODE - 항상 오프로드 모드 종료 - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - 실험 모드 켜짐 - - - CHILL MODE ON - 안정 모드 켜짐 - - - - ExternalStorageControl - - External Storage - 외부 저장소 - - - Extend your comma device's storage by inserting a USB drive into the aux port. - USB 드라이브를 AUX 포트에 삽입하여 comma 기기의 저장 공간을 확장합니다. - - - CHECK - 확인 - - - MOUNT - 마운트 - - - UNMOUNT - 마운트 해제 - - - FORMAT - 포맷 - - - Are you sure you want to format this drive? This will erase all data. - 이 드라이브를 포맷하시겠습니까? 이 작업은 모든 데이터를 지웁니다. - - - Format - 포맷 - - - formatting - 포맷 중 - - - insert drive - 드라이브 삽입 - - - needs format - 포맷 필요 - - - mounting - 마운트 중 - - - unmounting - 마운트 해제 중 - - - - FirehosePanel - - Firehose Mode: ACTIVE - Firehose 모드: 활성 - - - ACTIVE - 활성 - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>%n개 구간</b> 의 주행이 현재까지 학습 데이터셋에 포함되어 있습니다. - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>비활성</span>: 무제한 네트워크에 연결 하세요 - - - Firehose Mode - Firehose 모드 - - - sunnypilot learns to drive by watching humans, like you, drive. - -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. - sunnypilot은 당신과 같은 사람들의 운전을 보면서 스스로 운전을 배웁니다. - -Firehose 모드를 사용하면 훈련 데이터 업로드를 극대화하여 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 카드에 연결되어 있다면 운전 중에도 Firehose 모드를 사용할 수 있습니다.<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(및 특정 포크)만 훈련에 사용할 수 있습니다. - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - 최대 - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - 녹색 신호 - - - LEAD VEHICLE -DEPARTING - 전방 차량이 -출발하였습니다 - - - SPEED - SPEED - - - LIMIT - LIMIT - - - Near - 근처 - - - km - km - - - m - m - - - mi - mi - - - ft - ft - - - AHEAD - 전방 - - - MAX - 최대 - - - - HyundaiSettings - - Off - 사용 안 함 - - - Dynamic - 동적 - - - Predictive - 예측 - - - Custom Longitudinal Tuning - 사용자 지정 가감속 튜닝 - - - This feature can only be used with openpilot longitudinal control enabled. - 이 기능은 openpilot 가감속 제어가 활성화된 경우에만 사용할 수 있습니다. - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - 옵션을 선택하려면 기기 패널에서 "항상 오프로드"를 활성화하거나, 차량의 시동을 끄세요. - - - Off: Uses default tuning - 사용 안 함: 기본 튜닝을 사용합니다 - - - Dynamic: Adjusts acceleration limits based on current speed - 동적: 현재 속도에 따라 가속 제한을 조정합니다. - - - Predictive: Uses future trajectory data to anticipate needed adjustments - 예측: 향후 주행 궤적 데이터를 활용해 필요한 조정을 미리 파악합니다. - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - openpilot 가감속 제어의 가속 부드러움을 조정하여 주행 경험을 세밀하게 조정하세요. - - - - InputDialog - - Cancel - 취소 - - - Need at least %n character(s)! - - 최소 %n자 이상이어야 합니다! - - - - - 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. - 토글을 켜면 사각지대 모니터링(BSM)이 가로막는 차량을 감지할 때 차로 변경을 안전하게 수행할 수 있도록 지연 타이머를 활성화합니다. - - - - LateralPanel - - Modular Assistive Driving System (MADS) - 모듈형 보조 주행 시스템 (MADS) - - - Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - 인기 있는 MADS 기능을 활성화하세요. 토글을 끄면 기본 sunnypilot 작동/해제 방식으로 되돌아갑니다. - - - Customize MADS - MADS 사용자 지정 - - - Customize Lane Change - 차로 변경 사용자 지정 - - - Pause Lateral Control with Blinker - 방향지시등으로 조향 제어 일시 중지 - - - Pause lateral control with blinker when traveling below the desired speed selected. - 설정한 속도보다 낮은 속도로 주행 중일 때 방향지시등으로 조향 제어를 일시 중지합니다. - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - 차로 중앙 유지(ALC) 및 어댑티브 크루즈 컨트롤(ACC) 기능을 독립적으로 사용할 수 있습니다. - - - Start the vehicle to check vehicle compatibility. - 차량 호환성을 확인하려면 차량 시동을 걸어주세요. - - - This platform supports all MADS settings. - 이 플랫폼은 모든 MADS 설정을 지원합니다. - - - This platform supports limited MADS settings. - 이 플랫폼은 제한된 MADS 설정을 지원합니다. - - - Enforce Torque Lateral Control - 토크 조향 제어 강제 적용 - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - sunnypilot이 토크 조향 제어를 통해 조향하도록 강제하려면 이 기능을 활성화하세요. - - - Customize Params - 매개변수 사용자 지정 - - - - LongitudinalPanel - - Custom ACC Speed Increments - 사용자 지정 ACC 속도 조절 단위 - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - 크루즈 속도 증가/감소 시 짧게 누르기 및 길게 누르기 단위를 개별 설정합니다. - - - This feature can only be used with openpilot longitudinal control enabled. - 이 기능은 openpilot의 가감속 제어가 활성화되어 있어야만 사용할 수 있습니다. - - - This feature is not supported on this platform due to vehicle limitations. - 이 기능은 차량 제한으로 인해 이 플랫폼에서 지원되지 않습니다. - - - Start the vehicle to check vehicle compatibility. - 차량 호환성을 확인하려면 차량 시동을 걸어주세요. - - - Intelligent Cruise Button Management (ICBM) (Alpha) - 지능형 크루즈 버튼 관리 (ICBM) (알파) - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - 활성화하면, sunnypilot은 제한적인 가감속 제어를 위해 버튼 조작을 재현하여 내장 크루즈 컨트롤 버튼을 관리하려고 시도합니다. - - - Smart Cruise Control - Vision - 스마트 크루즈 컨트롤 - 비전 - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - 전방 커브 구간을 통과하기 위한 적절한 속도를 예측하기 위해 시각 경로 예측 기능을 사용합니다. - - - Smart Cruise Control - Map - 스마트 크루즈 컨트롤 - 지도 - - - Use map data to estimate the appropriate speed to drive through turns ahead. - 전방 커브 구간을 통과하기 위한 적절한 속도를 예측하기 위해 지도 데이터를 사용합니다. - - - Speed Limit - 속도 제한 - - - - MadsSettings - - Toggle with Main Cruise - 메인 크루즈와 연동 - - - Unified Engagement Mode (UEM) - 통합 주행 보조 모드 (UEM) - - - Steering Mode on Brake Pedal - 브레이크 조작 시 조향 모드 - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - 참고: LFA/LKAS 버튼이 없는 차량의 경우, 이 기능을 비활성화하면 조향 제어가 작동하지 않습니다. - - - 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. - 참고: 통합 주행 보조 모드(UEM)로 조향 제어가 작동하면, MADS 버튼으로 직접 끄거나 차량 시동을 끄기 전까지 계속 유지됩니다. - - - Start the vehicle to check vehicle compatibility. - 차량 호환성을 확인하려면 차량 시동을 걸어주세요. - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - 이 기능은 차량의 한계로 인해 기본적으로 비활성화되어 있으며, 선택할 수 없습니다. - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - 이 기능은 차량의 한계로 인해 기본적으로 활성화되어 있으며, 선택할 수 없습니다. - - - This platform only supports Disengage mode due to vehicle limitations. - 이 플랫폼은 차량의 한계로 인해 '해제 모드'만 지원합니다. - - - Remain Active - 계속 활성 유지 - - - Remain Active: ALC will remain active when the brake pedal is pressed. - 계속 활성 유지: 브레이크 페달을 밟아도 차로 유지 보조(ALC) 기능이 계속 작동합니다. - - - Pause - 일시 정지 - - - Pause: ALC will pause when the brake pedal is pressed. - 일시 정지: 브레이크 페달을 밟으면 차로 유지 보조(ALC) 기능이 일시 정지됩니다. - - - Disengage - 해제 - - - Disengage: ALC will disengage when the brake pedal is pressed. - 해제: 브레이크 페달을 밟으면 차로 유지 보조(ALC) 기능이 해제됩니다. - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - 브레이크 페달을 직접 밟았을 때 자동 차로 중앙 유지(ALC) 기능이 어떻게 작동할지 선택하세요. - - - - MaxTimeOffroad - - Max Time Offroad - 최대 오프로드 시간 - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - 차량 시동이 꺼지면 설정된 시간이 지난 후 기기가 자동으로 꺼집니다.<br/>(기본값은 30시간입니다) - - - Always On - 켜짐 유지 - - - h - 시간 - - - m - - - - (default) - (기본값) - - - - ModelsPanel - - Current Model - 현재 모델 - - - SELECT - 선택 - - - Clear Model Cache - 모델 캐시 지우기 - - - CLEAR - 지우기 - - - Driving Model - 주행 모델 - - - Navigation Model - 내비게이션 모델 - - - Vision Model - 비전 모델 - - - Policy Model - 정책 모델 - - - Live Learning Steer Delay - 조향 지연 실시간 학습 - - - Adjust Software Delay - 소프트웨어 지연 조절 - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - 조향 지연 실시간 학습 기능이 꺼져 있을 때, 소프트웨어 지연을 조절합니다. -기본값은 0.2입니다. - - - %1 - %2 - %1 - %2 - - - downloaded - 다운로드 완료 - - - ready - 준비 완료 - - - from cache - 캐시에서 가져옴 - - - download failed - %1 - 다운로드 실패 - %1 - - - pending - %1 - 대기 중 - %1 - - - Fetching models... - 모델 가져오는 중... - - - Select a Model - 모델 선택 - - - Default - 기본값 - - - Model download has started in the background. - 모델 다운로드가 백그라운드에서 시작되었습니다. - - - We STRONGLY suggest you to reset calibration. - 캘리브레이션을 초기화하는 것을 강력히 권장합니다. - - - Would you like to do that now? - 지금 바로 하시겠습니까? - - - Reset Calibration - 캘리브레이션 초기화 - - - Driving Model Selector - 주행 모델 선택기 - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - <u>현재 사용 중인 모델을 제외한</u><br/>모든 다운로드된 모델이 캐시에서 삭제됩니다.<br/><br/>계속 진행하시겠습니까? - - - Clear Cache - 캐시 지우기 - - - Warning: You are on a metered connection! - 경고: 데이터 사용량 제한이 있는 네트워크에 연결되어 있습니다! - - - Continue - 계속 - - - on Metered - 데이터 사용량 제한 네트워크 사용 - - - Cancel - 취소 - - - Refresh Model List - 모델 목록 새로고침 - - - REFRESH - 새로고침 - - - Fetching Latest Models - 최신 모델 불러오는 중 - - - 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. - 이 기능을 켜면 차량이 스스로 조향 응답 속도를 학습하고 맞춥니다. 끄면 고정된 조향 응답 속도를 사용합니다. 이 기능을 켜두는 것이 기본 openpilot 경험을 제공합니다. - - - Live Steer Delay: - 실시간 조향 지연 시간: - - - Actuator Delay: - 액츄에이터 지연 시간: - - - Software Delay: - 소프트웨어 지연 시간: - - - Total Delay: - 전체 지연 시간: - - - Use Lane Turn Desires - 차로 회전 의도 사용 - - - Adjust Lane Turn Speed - 차로 회전 속도 조정 - - - Set the maximum speed for lane turn desires. Default is 19 %1. - 차로 회전 의도의 최대 속도를 설정합니다. 기본값은 19 %1 입니다. - - - - MultiOptionDialog - - Select - 선택 - - - Cancel - 취소 - - - - Networking - - Advanced - 고급 설정 - - - Enter password - 비밀번호를 입력하세요 - - - for "%1" - "%1"에 접속하려면 비밀번호가 필요합니다 - - - Wrong password - 비밀번호가 틀렸습니다 - - - - NetworkingSP - - Scan - 검색 - - - Scanning... - 검색 중... - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - 신경망 기반 조향 제어 (NNLC) - - - NNLC is currently not available on this platform. - 이 플랫폼에서는 현재 NNLC를 사용할 수 없습니다. - - - Start the car to check car compatibility - 차량 호환성을 확인하려면 차량 시동을 걸어주세요. - - - NNLC Not Loaded - NNLC가 불러와지지 않음 - - - NNLC Loaded - NNLC가 불러와짐 - - - 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. - 과거 <b>"NNFF"</b>라고 불렸던 이 기능은 기존의 조향 <b>"토크"</b> 제어기를 대체합니다. 이는 각 차량의 주행 데이터(더 정확히는, 각기 다른 EPS 펌웨어 데이터)를 기반으로 훈련된 신경망을 사용하여 제어 정확도를 높이기 위함입니다. - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - sunnypilot 팀에게 연락하시려면, sunnypilot Discord 서버 내의 다음 채널을 이용해 주세요. - - - 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: - 그리고 차량에 NNLC가 활성화될 수 있도록 로그를 제공하려면: - - - - OffroadAlert - - Unable to download updates -%1 - 업데이트를 다운로드할 수 없습니다 -%1 - - - Taking camera snapshots. System won't start until finished. - 카메라 스냅샷을 촬영하는 중입니다. 완료될 때까지 시스템이 시작되지 않습니다. - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - 기기 운영체제 업데이트가 백그라운드에서 다운로드 중입니다. 설치가 준비되면 업데이트를 진행하라는 메시지가 표시됩니다. - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 기기 온도가 너무 높습니다. 시스템이 시작하기 전에 온도를 낮추고 있습니다. 현재 내부 부품 온도는 %1입니다. - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - 기기가 comma.ai 서버에 등록되지 못했습니다. 따라서 comma.ai 서버에 연결하거나 데이터를 업로드할 수 없으며, comma.ai의 지원도 받을 수 없습니다. 만약 이 기기를 comma.ai/shop에서 구매했다면, https://comma.ai/support에서 지원 티켓을 열어주세요. - - - Acknowledge Excessive Actuation - 과도한 작동을 인정하십시오 - - - Snooze Update - 업데이트 일시 중지 - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - openpilot이 지난 주행에서 과도한 %1 작동을 감지했습니다. 문제 해결을 위해 https://comma.ai/support로 지원팀에 연락하시고, 사용 중인 기기의 동글 ID를 공유해 주세요. - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - 즉시 인터넷에 연결하여 업데이트를 확인하세요. 인터넷에 연결하지 않으면, %1 후 sunnypilot이 비활성화됩니다. - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 업데이트를 확인하려면 인터넷에 연결해 주세요. 인터넷에 연결하여 업데이트를 확인할 때까지 sunnypilot은 자동으로 시작되지 않습니다. - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - sunnypilot이 당신의 차량을 식별하지 못했습니다. 현재 차량은 지원되지 않거나, 차량의 ECU가 인식되지 않습니다. 올바른 차량에 펌웨어 버전을 추가하는 풀 리퀘스트를 제출해 주세요. 도움이 필요하면 discord.comma.ai에 참여하세요. - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot이 기기의 장착 위치 변화를 감지했습니다. 기기가 마운트에 완전히 장착되었는지, 그리고 마운트가 앞유리에 단단히 고정되었는지 확인하세요. - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - OpenStreetMap 데이터베이스가 최신이 아닙니다. 향상된 속도 제어 및 도로명 표시를 위해 OpenStreetMap 데이터를 계속 사용하려면, 새로운 지도를 다운로드해야 합니다. - -%1 - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - <b>지원되지 않는 브랜치 감지</b> - 현재 사용 중인 <b><u>%1</u></b> 브랜치는 comma three 기기에서 더 이상 지원되지 않습니다. <b>[기기 > 소프트웨어]</b>로 이동하여 브랜치 이름에 <b><u>-tici</u></b> 가 포함된 comma three용 브랜치로 설치하세요. - - - - OffroadHome - - UPDATE - 업데이트 - - - ALERTS - 알림 - - - ALERT - 알림 - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - 항상 오프로드 활성 상태 - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - 즉시 제어하세요 - - - Reboot Device - 기기 재부팅 - - - Waiting to start - 시작 대기 중 - - - System Unresponsive - 시스템 응답 없음 - - - sunnypilot Unavailable - sunnypilot 사용 불가 - - - - OsmPanel - - Mapd Version - Mapd 버전 - - - Offline Maps ETA - 오프라인 지도 남은 예상 시간 - - - Time Elapsed - 경과 시간 - - - Downloaded Maps - 다운로드된 지도 - - - DELETE - 삭제 - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - 다운로드된 모든 지도가 삭제됩니다. - -모든 지도를 정말로 삭제하시겠습니까? - - - Yes, delete all the maps. - 네, 모든 지도를 삭제합니다. - - - Database Update - 데이터베이스 업데이트 - - - CHECK - 확인 - - - Country - 국가 - - - SELECT - 선택 - - - Fetching Country list... - 국가 목록 불러오는 중... - - - State - - - - Fetching State list... - 주 목록 불러오는 중... - - - All - 전체 - - - REFRESH - 새로 고침 - - - UPDATE - 업데이트 - - - Download starting... - 다운로드 시작 중... - - - Error: Invalid download. Retry. - 오류: 유효하지 않은 다운로드입니다. 다시 시도해 주세요. - - - Download complete! - 다운로드 완료! - - - - -Warning: You are on a metered connection! - - -경고: 데이터 사용량 제한이 있는 네트워크에 연결되어 있습니다! - - - This will start the download process and it might take a while to complete. - 다운로드 절차를 시작합니다. 완료하는 데 시간이 다소 걸릴 수 있습니다. - - - Continue on Metered - 데이터 사용량 제한 네트워크로 계속 - - - Start Download - 다운로드 시작 - - - m - - - - s - - - - Calculating... - 계산 중... - - - Downloaded - 다운로드됨 - - - Calculating ETA... - 남은 예상 시간 계산 중... - - - Ready - 준비 완료 - - - Time remaining: - 남은 시간: - - - - PairingPopup - - Pair your device to your comma account - 기기를 comma 계정에 페어링하세요. - - - Go to https://connect.comma.ai on your phone - 휴대폰에서 https://connect.comma.ai로 이동하세요. - - - Click "add new device" and scan the QR code on the right - "새 기기 추가"를 누르고 오른쪽에 있는 QR 코드를 스캔하세요. - - - Bookmark connect.comma.ai to your home screen to use it like an app - connect.comma.ai를 홈 화면에 북마크하여 앱처럼 사용하세요. - - - Please connect to Wi-Fi to complete initial pairing - 초기 페어링을 완료하려면 Wi-Fi에 연결해 주세요. - - - - ParamControl - - Cancel - 취소 - - - Enable - 활성화 - - - - ParamControlSP - - Enable - 활성화 - - - Cancel - 취소 - - - - PlatformSelector - - Vehicle - 차량 - - - SEARCH - 검색 - - - Search your vehicle - 차량 검색 - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - 차량 연식(예: 2021)과 모델명(예: Toyota Corolla)을 입력하세요: - - - SEARCHING - 검색 중 - - - REMOVE - 제거 - - - This setting will take effect immediately. - 이 설정은 즉시 적용됩니다. - - - This setting will take effect once the device enters offroad state. - 이 설정은 기기가 오프로드 상태에 진입하면 적용됩니다. - - - Vehicle Selector - 차량 선택기 - - - Confirm - 확인 - - - Cancel - 취소 - - - No vehicles found for query: %1 - 입력한 정보와 일치하는 차량을 찾을 수 없습니다: %1 - - - Select a vehicle - 차량 선택 - - - Unrecognized Vehicle - 인식할 수 없는 차량 - - - Fingerprinted automatically - 자동으로 지문 인식됨 - - - Manually selected - 수동으로 선택됨 - - - Not fingerprinted or manually selected - 지문 인식이 되지 않았거나 수동으로 선택되지 않음 - - - Select vehicle to force fingerprint manually. - 수동으로 지문 인식을 강제할 차량을 선택하세요. - - - Colors represent fingerprint status: - 색상은 지문 인식 상태를 나타냅니다: - - - - PrimeAdWidget - - Upgrade Now - 지금 업그레이드하세요 - - - Become a comma prime member at connect.comma.ai - connect.comma.ai에서 comma 프라임 회원이 되세요. - - - PRIME FEATURES: - 프라임 기능: - - - Remote access - 원격 접속 - - - 24/7 LTE connectivity - 24시간 7일 LTE 연결 - - - 1 year of drive storage - 1년치 주행 기록 저장 공간 - - - Remote snapshots - 원격 스냅샷 - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ 구독됨 - - - comma prime - comma 프라임 - - - - QObject - - %n minute(s) ago - - %n 분 전 - - - - %n hour(s) ago - - %n 시간 전 - - - - %n day(s) ago - - %n 일 전 - - - - now - now - - - sunnypilot - sunnypilot - - - - SettingsWindow - - × - × - - - Device - 기기 - - - Network - 네트워크 - - - Toggles - 토글 - - - Software - 소프트웨어 - - - Developer - 개발자 - - - Firehose - Firehose - - - - SettingsWindowSP - - × - × - - - Device - 기기 - - - Network - 네트워크 - - - sunnylink - sunnylink - - - Toggles - 토글 - - - Software - 소프트웨어 - - - Models - 모델 - - - Steering - 조향 - - - Cruise - 크루즈 - - - Visuals - 시각 효과 - - - OSM - OSM - - - Trips - 주행 기록 - - - Vehicle - 차량 - - - Firehose - Firehose - - - Developer - 개발자 - - - Display - 화면 - - - - SetupWidget - - Finish Setup - 설정 완료하기 - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 기기를 comma connect (connect.comma.ai)와 페어링하고 comma prime 혜택을 받으세요. - - - Pair device - 기기 페어링 - - - - Sidebar - - CONNECT - CONNECT - - - OFFLINE - 오프라인 - - - ONLINE - 온라인 - - - ERROR - 오류 - - - TEMP - 온도 - - - HIGH - 높음 - - - GOOD - 정상 - - - OK - 양호 - - - VEHICLE - 차량 - - - NO - 판다 - - - PANDA - 없음 - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - 이더넷 - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - 비활성화됨 - - - OFFLINE - 오프라인 - - - REGIST... - 등록 중... - - - ONLINE - 온라인 - - - ERROR - 오류 - - - SUNNYLINK - SUNNYLINK - - - - SoftwarePanel - - Updates are only downloaded while the car is off. - 업데이트는 차량 시동이 꺼져 있을 때만 다운로드됩니다. - - - Current Version - 현재 버전 - - - Download - 다운로드 - - - Install Update - 업데이트 설치 - - - INSTALL - 설치 - - - Target Branch - 대상 브랜치 - - - SELECT - 선택 - - - Select a branch - 브랜치 선택 - - - UNINSTALL - 제거 - - - Uninstall %1 - %1 제거 - - - Are you sure you want to uninstall? - 정말로 제거하시겠습니까? - - - CHECK - 확인 - - - Uninstall - 제거 - - - failed to check for update - 업데이트 확인 실패 - - - up to date, last checked %1 - 최신 버전입니다. 마지막 확인: %1 - - - DOWNLOAD - 다운로드 - - - update available - 업데이트 가능 - - - never - 확인 안함 - - - - SoftwarePanelSP - - Search Branch - 브랜치 검색 - - - Enter search keywords, or leave blank to list all branches. - 검색어를 입력하거나, 비워두면 모든 브랜치가 표시됩니다. - - - Disable Updates - 업데이트 비활성화 - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - 이 기능을 활성화하면 소프트웨어 업데이트가 비활성화됩니다. <b>적용하려면 재부팅이 필요합니다.</b> - - - No branches found for keywords: %1 - 입력한 키워드와 일치하는 브랜치을 찾을 수 없습니다: %1 - - - Select a branch - 브랜치 선택 - - - %1 updates requires a reboot.<br>Reboot now? - 업데이트 기능 %1는 재부팅이 필요합니다.<br>지금 재부팅하시겠습니까? - - - Reboot - 재부팅 - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - 이 기능을 활성화하면 소프트웨어 업데이트가 비활성화됩니다.<br><b>적용하려면 재부팅이 필요합니다.</b> - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - 이 설정들을 변경하려면 항상 오프로드 모드를 활성화하거나 차량 시동을 꺼야 합니다. - - - - SpeedLimitPolicy - - Back - 뒤로 - - - Speed Limit Source - 속도 제한 출처 - - - ⦿ Car Only: Use Speed Limit data only from Car - ⦿ 차량만: 차량에서 제공되는 속도 제한 데이터만 사용 - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - ⦿ 지도만: OpenStreetMaps에서 제공되는 속도 제한 데이터만 사용 - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - ⦿ 차량 우선: 사용 가능한 경우 차량의 속도 제한 데이터를 사용하고, 그렇지 않은 경우 OpenStreetMaps의 데이터 사용 - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - ⦿ 지도 우선: 사용 가능한 경우 OpenStreetMaps의 속도 제한 데이터를 사용하고, 그렇지 않은 경우 차량의 데이터 사용 - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - ⦿ 결합: 차량 및 OpenStreetMaps의 속도 제한 결합 데이터 사용 - - - Car -Only - 차량만 - - - Map -Only - 지도만 - - - Car -First - 차량 -우선 - - - Map -First - 지도 -우선 - - - Combined -Data - 결합 -데이터 - - - - SpeedLimitSettings - - Back - 뒤로 - - - Speed Limit - 속도 제한 - - - Customize Source - 출처 사용자 지정 - - - Speed Limit Offset - 속도 제한 오프셋 - - - ⦿ None: No Offset - ⦿ 없음: 오프셋 없음 - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - ⦿ 고정: 고정 오프셋을 추가합니다. [제한 속도 + 오프셋] - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - ⦿ 비율: 백분율 오프셋을 추가합니다. [제한 속도 + (오프셋 % 제한 속도)] - - - ⦿ Off: Disables the Speed Limit functions. - ⦿ 끄기: 속도 제한 기능을 비활성화합니다. - - - ⦿ Information: Displays the current road's speed limit. - ⦿ 정보: 현재 도로의 제한 속도를 표시합니다. - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - ⦿ 경고: 현재 도로의 제한 속도를 초과할 경우 경고합니다. - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - ⦿ 보조: +/- 버튼을 조작할 때 현재 도로의 제한 속도를 기준으로 차량의 크루즈 속도를 조정합니다. - - - None - 없음 - - - Fixed - 고정 - - - Percent - 비율 - - - Off - 끄기 - - - Information - 정보 - - - Warning - 경고 - - - Assist - 보조 - - - - SshControl - - SSH Keys - SSH 키 - - - 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 사용자 이름을 추가하라고 요청하지 않을 것입니다. - - - ADD - 추가 - - - Enter your GitHub username - GitHub 사용자 이름을 입력하세요 - - - LOADING - 로딩 중 - - - REMOVE - 제거 - - - Username '%1' has no keys on GitHub - GitHub에 사용자 이름 '%1'의 키가 없습니다. - - - Request timed out - 요청 시간 초과 - - - Username '%1' doesn't exist on GitHub - GitHub에 사용자 이름 '%1'이(가) 존재하지 않습니다. - - - - SshToggle - - Enable SSH - SSH 사용 - - - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - 이것은 마스터 스위치입니다. 원할 경우 모든 sunnylink 요청을 차단할 수 있습니다. - - - Enable sunnylink - sunnylink 사용 - - - Sponsor Status - 후원 상태 - - - SPONSOR - 후원 - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - sunnypilot의 후원자가 되어, sunnylink 기능이 출시되면 얼리 액세스 권한을 얻으세요. - - - Pair GitHub Account - GitHub 계정 페어링 - - - PAIR - 페어링 - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - GitHub 계정을 페어링하여 sunnylink의 API 접근을 포함한 후원자 혜택을 기기에 부여하세요. - - - N/A - N/A - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - sunnylink 동글 ID를 찾을 수 없습니다. 인터넷 연결이 약하거나 sunnylink 등록 문제 때문일 수 있습니다. 재부팅 후 다시 시도해 주세요. - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - 🎉돌아오신 것을 환영합니다! sunnylink를 다시 활성화하셨군요! 🚀 - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - 솔직히 말해서, sunnylink를 비활성화한 것을 보니 좀 슬프네요 😢. 하지만 돌아올 준비가 되면 저희는 언제든 여기 있을게요 🎉. - - - Backup Settings - 설정 백업 - - - Are you sure you want to backup sunnypilot settings? - sunnypilot 설정을 백업하시겠습니까? - - - Back Up - 백업 - - - Restore Settings - 설정 복원 - - - Are you sure you want to restore the last backed up sunnypilot settings? - 마지막으로 백업된 sunnypilot 설정을 복원하시겠습니까? - - - Restore - 복원 - - - Backup in progress %1% - 백업 진행 중 %1% - - - Backup Failed - 백업 실패 - - - Settings backup completed. - 설정 백업 완료. - - - Restore in progress %1% - 복원 진행 중 %1% - - - Restore Failed - 복원 실패 - - - Unable to restore the settings, try again later. - 설정을 복원할 수 없습니다. 나중에 다시 시도해 주세요. - - - Settings restored. Confirm to restart the interface. - 설정이 복원되었습니다. 인터페이스를 재시작하려면 확인하세요. - - - Device ID - 기기 ID - - - THANKS ♥ - 감사합니다 ♥ - - - Not Sponsor - 후원자 아님 - - - Paired - 페어링됨 - - - Not Paired - 페어링되지 않음 - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - sunnylink 업로더를 활성화하여 sunnypilot이 귀하의 주행 데이터를 sunnypilot 서버로 업로드하도록 허용합니다. (가장 높은 등급에서만 해당하며, 사용자에게는 어떠한 이점도 제공하지 않습니다. 저희는 그냥 데이터 용량을 테스트하는 중입니다.) - - - [Don't use] Enable sunnylink uploader - [사용하지 마세요] sunnylink 업로더 사용 - - - 🚀 sunnylink 🚀 - 🚀 sunnylink 🚀 - - - For secure backup, restore, and remote configuration - 안전한 백업, 복원 및 원격 설정을 위해 - - - Sponsorship isn't required for basic backup/restore - 기본적인 백업/복원은 후원이 필요하지 않습니다 - - - Click the sponsor button for more details - 후원 버튼을 눌러 더 자세한 정보를 확인하세요 - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - GitHub 계정에 로그인하려면 QR 코드를 스캔하세요. - - - Follow the prompts to complete the pairing process - 안내에 따라 페어링 과정을 완료하세요. - - - Re-enter the "sunnylink" panel to verify sponsorship status - "sunnylink" 패널에 다시 들어가서 후원자 상태를 확인하세요. - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - 후원자 상태가 업데이트되지 않았다면, https://discord.gg/sunnypilot에서 운영자에게 연락해 주세요. - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - sunnyhaibin의 GitHub Sponsors 페이지를 방문하려면 QR 코드를 스캔하세요. - - - 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 - https://discord.gg/sunnypilot에서 우리 커뮤니티에 참여하고, 운영자에게 후원자 상태를 확인해 주세요. - - - Pair your GitHub account - GitHub 계정 페어링 - - - Early Access: Become a sunnypilot Sponsor - 얼리 액세스: sunnypilot 후원자가 되세요 - - - - TermsPage - - Decline - 거절 - - - Agree - 동의 - - - Welcome to sunnypilot - sunnypilot에 오신 것을 환영합니다 - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - sunnypilot을 사용하려면 이용 약관에 동의해야 합니다. 계속하기 전에 <span style='color: #465BEA;'>https://comma.ai/terms</span>에서 최신 약관을 읽어보세요. - - - - TogglesPanel - - Enable Lane Departure Warnings - 차선 이탈 경고 활성화 - - - Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). - 차량이 50km/h(31mph) 이상의 속도로 주행할 때 방향지시등이 켜지지 않은 상태에서 차선을 벗어나면 경고합니다. - - - Use Metric System - 미터법 사용 - - - Display speed in km/h instead of mph. - mph 대신 km/h로 속도를 표시합니다. - - - Record and Upload Driver Camera - 운전자 카메라 녹화 및 업로드 - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - 운전자 카메라의 영상 데이터를 업로드하여 운전자 모니터링 알고리즘을 개선합니다. - - - Disengage on Accelerator Pedal - 가속페달 조작 시 해제 - - - Experimental Mode - 실험 모드 - - - New Driving Visualization - 새로운 주행 시각화 - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 차량의 순정 ACC가 가감속 제어에 사용되기 때문에, 이 차에서는 현재 실험 모드를 사용할 수 없습니다. - - - openpilot longitudinal control may come in a future update. - openpilot의 가감속 제어 기능은 향후 업데이트에서 제공될 수 있습니다. - - - Aggressive - 공격적 - - - Standard - 표준 - - - Relaxed - 느긋한 - - - Driving Personality - 주행 성향 - - - End-to-End Longitudinal Control - End-to-End 가감속 제어 - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - 주행 시각화 화면은 저속에서 일부 회전 구간을 더 잘 보여주기 위해 전방 광각 카메라로 전환됩니다. 또한, 실험 모드 로고가 오른쪽 상단에 표시됩니다. - - - Always-On Driver Monitoring - 상시 운전자 모니터링 - - - Changing this setting will restart openpilot if the car is powered on. - 이 설정을 변경하면 차량 전원이 켜져 있을 때 openpilot이 재시작됩니다. - - - Record and Upload Microphone Audio - 마이크 오디오 녹음 및 업로드 - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - 운전 중 마이크 오디오를 녹음하고 저장합니다. 이 오디오는 comma connect의 대시캠 영상에 포함됩니다. - - - 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. - 어댑티브 크루즈 컨트롤 및 차로 유지 운전자 보조를 위해 sunnypilot 시스템을 사용하세요. 이 기능을 사용하는 동안에는 항상 주의를 기울여야 합니다. - - - Enable Dynamic Experimental Control - 동적 실험 제어 활성화 - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - 모델이 sunnypilot ACC 또는 sunnypilot End to End 가감속 제어를 언제 사용할지 결정하도록 토글을 활성화하세요. - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - 활성화하면, 가속 페달을 밟을 때 sunnypilot이 해제됩니다. - - - Enable driver monitoring even when sunnypilot is not engaged. - sunnypilot이 해제되어 있을 때도 운전자 모니터링을 사용합니다. - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 표준 모드가 권장됩니다. 공격적 모드에서는 sunnypilot이 앞차를 더 가깝게 따라가고 가속 및 제동에 더 적극적으로 반응합니다. 느긋한 모드에서는 sunnypilot이 앞차로부터 더 멀리 떨어져 주행합니다. 지원되는 차량에서는 핸들의 거리 조절 버튼으로 이 주행 성향들을 순환하며 변경할 수 있습니다. - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot은 기본적으로 <b>안정 모드</b>로 주행합니다. 실험 모드는 안정 모드에서 사용하기에는 아직 준비되지 않은 <b>알파 수준 기능</b>을 활성화합니다. 실험 기능은 아래에 나열되어 있습니다: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - 주행 모델이 가속 페달과 브레이크를 제어하도록 허용합니다. sunnypilot은 신호등과 정지 표지판에서 정지하는 것을 포함하여 사람이 운전하는 것처럼 주행할 것입니다. 주행 모델이 운전 속도를 결정하므로, 설정 속도는 상한선 역할만 합니다. 이것은 알파 품질의 기능입니다. 실수가 예상되니 주의해 주세요. - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - sunnypilot 가감속 제어의 알파 버전은 실험 모드와 함께 비공개(비 릴리스) 브랜치에서 테스트할 수 있습니다. - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 실험 모드를 활성화하려면 sunnypilot 가감속 제어(알파) 토글을 켜세요. - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - 실시간 수동 튜닝 - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - 토크 조향 제어기가 셀프 튜닝에서 학습된 값 대신 고정된 값을 사용하도록 강제합니다. 이 토글을 활성화하면 셀프 튜닝 값이 무시됩니다. - - - Lateral Acceleration Factor - 조향 가속 계수 - - - Friction - 마찰 - - - Real-time and Offline - 실시간 및 오프라인 - - - Offline Only - 오프라인만 - - - - TorqueLateralControlSettings - - Self-Tune - 셀프 튜닝 - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - 기본적으로 토크 조향 제어를 사용하지 않는 플랫폼에 대해 토크 조향 제어의 셀프 튜닝 기능을 활성화합니다. - - - Less Restrict Settings for Self-Tune (Beta) - 셀프 튜닝 제한 완화 설정 (베타) - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - 셀프 튜닝 사용 시 덜 엄격한 설정을 사용합니다. 이는 학습 값에 대해 torqued가 더 관대하도록 허용합니다. - - - Enable Custom Tuning - 사용자 지정 튜닝 사용 - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - 토크 조향 제어에 대한 사용자 지정 튜닝을 활성화합니다. 아래에서 조향 가속 계수 및 마찰을 수정하면 "opendbc/car/torque_data" 내의 YAML 파일에 명시된 오프라인 값이 무시됩니다. 이 값들은 "실시간 수동 튜닝" 토글이 활성화되면 실시간으로도 사용됩니다. - - - - TreeOptionDialog - - Select - 선택 - - - Cancel - 취소 - - - Favorites - 즐겨찾기 - - - - VisualsPanel - - Show Blind Spot Warnings - 사각지대 경고 표시 - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - 이 기능을 활성화하면, 차량에 BSM(사각지대 모니터링) 기능이 지원되는 한 사각지대에 차량이 감지되었을 때 경고가 표시됩니다. - - - Changing this setting will restart openpilot if the car is powered on. - 이 설정을 변경하면 차량 전원이 켜져 있을 때 openpilot이 재시작됩니다. - - - Off - 끄기 - - - Distance - 거리 - - - Speed - 속도 - - - Time - 시간 - - - All - 전체 - - - Display Metrics Below Chevron - 역삼각형 아래에 측정 지표 표시 - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - 앞차를 추적하는 역삼각형 아래에 유용한 측정 지표를 표시합니다. (openpilot 가감속 제어 기능이 있는 차량에만 해당됩니다) - - - Enable Tesla Rainbow Mode - 테슬라 무지개 모드 사용 - - - A beautiful rainbow effect on the path the model wants to take. - 모델이 가고자 하는 경로에 아름다운 무지개 효과를 추가합니다. - - - It - 이는 운전에 - - - does not - 어떠한 - - - affect driving in any way. - 영향도 미치지 않습니다. - - - Enable Standstill Timer - 정차 타이머 사용 - - - Show a timer on the HUD when the car is at a standstill. - 차량이 정차 상태일 때 HUD에 타이머를 표시합니다. - - - Display Road Name - 도로 이름 표시 - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - 차량이 주행 중인 도로의 이름을 표시합니다. 도로 이름을 가져오려면 OSM 패널에서 해당 위치의 OpenStreetMap 데이터베이스를 다운로드해야 합니다. - - - Green Traffic Light Alert (Beta) - 녹색 신호 감지 알림 (베타) - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - 앞 신호등이 초록 신호로 바뀌고 전방에 차량이 없을 때, 알림음과 화면 경고가 표시됩니다. - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - 참고: 이 알림음은 단순한 알림 목적으로만 설계되었습니다. 주변 환경을 살피고 그에 따라 결정하는 것은 운전자의 책임입니다. - - - Lead Departure Alert (Beta) - 전방 차량 출발 알림 (베타) - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - 당신이 정차 중이고 전방 차량이 움직이기 시작할 때, 알림음과 화면 경고가 표시됩니다. - - - Speedometer: Always Display True Speed - 속도계: 항상 실제 속도 표시 - - - Always display the true vehicle current speed from wheel speed sensors. - 항상 휠 스피드 센서에서 얻은 실제 차량 속도를 표시합니다. - - - Speedometer: Hide from Onroad Screen - 속도계: 주행 화면에서 숨기기 - - - Right - 오른쪽 - - - Right && -Bottom - 오른쪽 && -아래 - - - Developer UI - 개발자 UI - - - Display real-time parameters and metrics from various sources. - 다양한 출처의 실시간 매개변수 및 측정 지표를 표시합니다. - - - - 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 모드 <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - 네트워크 검색 중... - - - CONNECTING... - 연결 중... - - - FORGET - 삭제 - - - Forget Wi-Fi Network "%1"? - Wi-Fi 네트워크 "%1"을(를) 삭제하시겠습니까? - - - Forget - 삭제 - - - diff --git a/selfdrive/ui/translations/main_nl.ts b/selfdrive/ui/translations/main_nl.ts deleted file mode 100644 index 2bfc836773..0000000000 --- a/selfdrive/ui/translations/main_nl.ts +++ /dev/null @@ -1,1120 +0,0 @@ - - - - - AbstractAlert - - Close - Sluit - - - Snooze Update - Update uitstellen - - - Reboot and Update - Opnieuw Opstarten en Updaten - - - - AdvancedNetworking - - Back - Terug - - - Enable Tethering - Tethering Inschakelen - - - Tethering Password - Tethering Wachtwoord - - - EDIT - AANPASSEN - - - Enter new tethering password - Voer nieuw tethering wachtwoord in - - - IP Address - IP Adres - - - Enable Roaming - Roaming Inschakelen - - - APN Setting - APN Instelling - - - Enter APN - Voer APN in - - - leave blank for automatic configuration - laat leeg voor automatische configuratie - - - Cellular Metered - - - - Prevent large data uploads when on a metered connection - - - - - AnnotatedCameraWidget - - km/h - km/u - - - mph - mph - - - MAX - MAX - - - SPEED - SPEED - - - LIMIT - LIMIT - - - - ConfirmationDialog - - Ok - Ok - - - Cancel - Annuleren - - - - DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - U moet de Algemene Voorwaarden accepteren om sunnypilot te gebruiken. - - - Back - Terug - - - Decline, uninstall %1 - Afwijzen, verwijder %1 - - - - DevicePanel - - Dongle ID - Dongle ID - - - N/A - Nvt - - - Serial - Serienummer - - - Driver Camera - Bestuurders Camera - - - PREVIEW - BEKIJKEN - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - Bekijk de naar de bestuurder gerichte camera om ervoor te zorgen dat het monitoren van de bestuurder goed zicht heeft. (Voertuig moet uitgschakeld zijn) - - - Reset Calibration - Kalibratie Resetten - - - RESET - RESET - - - Are you sure you want to reset calibration? - Weet u zeker dat u de kalibratie wilt resetten? - - - Review Training Guide - Doorloop de Training Opnieuw - - - REVIEW - BEKIJKEN - - - 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? - Weet u zeker dat u de training opnieuw wilt doorlopen? - - - Regulatory - Regelgeving - - - VIEW - BEKIJKEN - - - Change Language - Taal Wijzigen - - - CHANGE - WIJZIGEN - - - Select a language - Selecteer een taal - - - Reboot - Opnieuw Opstarten - - - Power Off - Uitschakelen - - - 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. - Uw apparaat is gericht op %1° %2 en %3° %4. - - - down - omlaag - - - up - omhoog - - - left - links - - - right - rechts - - - Are you sure you want to reboot? - Weet u zeker dat u opnieuw wilt opstarten? - - - Disengage to Reboot - Deactiveer sunnypilot om opnieuw op te starten - - - Are you sure you want to power off? - Weet u zeker dat u wilt uitschakelen? - - - Disengage to Power Off - Deactiveer sunnypilot om uit te schakelen - - - - DriveStats - - Drives - Ritten - - - Hours - Uren - - - ALL TIME - TOTAAL - - - PAST WEEK - AFGELOPEN WEEK - - - KM - Km - - - Miles - Mijl - - - - DriverViewScene - - camera starting - Camera wordt gestart - - - - InputDialog - - Cancel - Annuleren - - - Need at least %n character(s)! - - Heeft minstens %n karakter nodig! - Heeft minstens %n karakters nodig! - - - - - Installer - - Installing... - Installeren... - - - Receiving objects: - Objecten ontvangen: - - - Resolving deltas: - Deltas verwerken: - - - Updating files: - Bestanden bijwerken: - - - - MapETA - - eta - eta - - - min - min - - - hr - uur - - - km - km - - - mi - mi - - - - MapInstructions - - km - km - - - m - m - - - mi - mi - - - ft - ft - - - - MapPanel - - Current Destination - Huidige Bestemming - - - CLEAR - LEEGMAKEN - - - Recent Destinations - Recente Bestemmingen - - - Try the Navigation Beta - Probeer de Navigatie Bèta - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - Krijg stapsgewijze routebeschrijving en meer met een comma -prime abonnement. Meld u nu aan: https://connect.comma.ai - - - No home -location set - Geen thuislocatie -ingesteld - - - No work -location set - Geen werklocatie -ingesteld - - - no recent destinations - geen recente bestemmingen - - - - MapWindow - - Map Loading - Kaart wordt geladen - - - Waiting for GPS - Wachten op GPS - - - - MultiOptionDialog - - Select - Selecteer - - - Cancel - Annuleren - - - - Networking - - Advanced - Geavanceerd - - - Enter password - Voer wachtwoord in - - - for "%1" - voor "%1" - - - Wrong password - Verkeerd wachtwoord - - - - OffroadHome - - UPDATE - UPDATE - - - ALERTS - WAARSCHUWINGEN - - - ALERT - WAARSCHUWING - - - - PairingPopup - - Pair your device to your comma account - Koppel uw apparaat aan uw comma-account - - - Go to https://connect.comma.ai on your phone - Ga naar https://connect.comma.ai op uw telefoon - - - Click "add new device" and scan the QR code on the right - Klik op "add new device" en scan de QR-code aan de rechterkant - - - Bookmark connect.comma.ai to your home screen to use it like an app - Voeg connect.comma.ai toe op uw startscherm om het als een app te gebruiken - - - - PrimeAdWidget - - Upgrade Now - Upgrade nu - - - Become a comma prime member at connect.comma.ai - Word een comma prime lid op connect.comma.ai - - - PRIME FEATURES: - PRIME BEVAT: - - - Remote access - Toegang op afstand - - - 1 year of storage - 1 jaar lang opslag - - - Developer perks - Voordelen voor ontwikkelaars - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ GEABONNEERD - - - comma prime - comma prime - - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - COMMA PUNTEN - - - - QObject - - Reboot - Opnieuw Opstarten - - - Exit - Afsluiten - - - dashcam - dashcam - - - sunnypilot - sunnypilot - - - %n minute(s) ago - - %n minuut geleden - %n minuten geleden - - - - %n hour(s) ago - - %n uur geleden - %n uur geleden - - - - %n day(s) ago - - %n dag geleden - %n dagen geleden - - - - - Reset - - Reset failed. Reboot to try again. - Opnieuw instellen mislukt. Start opnieuw op om opnieuw te proberen. - - - Are you sure you want to reset your device? - Weet u zeker dat u uw apparaat opnieuw wilt instellen? - - - Resetting device... - Apparaat opnieuw instellen... - - - System Reset - Systeemreset - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Systeemreset geactiveerd. Druk op bevestigen om alle inhoud en instellingen te wissen. Druk op Annuleren om het opstarten te hervatten. - - - Cancel - Annuleren - - - Reboot - Opnieuw Opstarten - - - Confirm - Bevestigen - - - Unable to mount data partition. Press confirm to reset your device. - Kan gegevenspartitie niet koppelen. Druk op bevestigen om uw apparaat te resetten. - - - - RichTextDialog - - Ok - Ok - - - - SettingsWindow - - × - × - - - Device - Apparaat - - - Network - Netwerk - - - Toggles - Opties - - - Software - Software - - - Navigation - Navigatie - - - - Setup - - WARNING: Low Voltage - WAARCHUWING: Lage Spanning - - - Power your device in a car with a harness or proceed at your own risk. - Voorzie uw apparaat van stroom in een auto met een harnas (car harness) of ga op eigen risico verder. - - - Power off - Uitschakelen - - - Continue - Doorgaan - - - Getting Started - Aan de slag - - - Before we get on the road, let’s finish installation and cover some details. - Laten we, voordat we op pad gaan, de installatie afronden en enkele details bespreken. - - - Connect to Wi-Fi - Maak verbinding met Wi-Fi - - - Back - Terug - - - Continue without Wi-Fi - Doorgaan zonder Wi-Fi - - - Waiting for internet - Wachten op internet - - - Choose Software to Install - Kies Software om te Installeren - - - Dashcam - Dashcam - - - Custom Software - Andere Software - - - Enter URL - Voer URL in - - - for Custom Software - voor Andere Software - - - Downloading... - Downloaden... - - - Download Failed - Downloaden Mislukt - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Zorg ervoor dat de ingevoerde URL geldig is en dat de internetverbinding van het apparaat goed is. - - - Reboot device - Apparaat opnieuw opstarten - - - Start over - Begin opnieuw - - - - SetupWidget - - Finish Setup - Installatie voltooien - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Koppel uw apparaat met comma connect (connect.comma.ai) en claim uw comma prime-aanbieding. - - - Pair device - Apparaat koppelen - - - - Sidebar - - CONNECT - VERBINDING - - - OFFLINE - OFFLINE - - - ONLINE - ONLINE - - - ERROR - FOUT - - - TEMP - TEMP - - - HIGH - HOOG - - - GOOD - GOED - - - OK - OK - - - VEHICLE - VOERTUIG - - - NO - GEEN - - - PANDA - PANDA - - - GPS - GPS - - - SEARCH - ZOEKEN - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - 4G - - - 5G - 5G - - - - SoftwarePanel - - Git Branch - Git Branch - - - Git Commit - Git Commit - - - OS Version - OS Versie - - - Version - Versie - - - Last Update Check - Laatste Updatecontrole - - - 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 - Controleer op Updates - - - CHECKING - CONTROLEER - - - Switch Branch - Branch Verwisselen - - - ENTER - INVOEREN - - - The new branch will be pulled the next time the updater runs. - Tijdens de volgende update wordt de nieuwe branch opgehaald. - - - Enter branch name - Voer branch naam in - - - Uninstall %1 - Verwijder %1 - - - UNINSTALL - VERWIJDER - - - Are you sure you want to uninstall? - Weet u zeker dat u de installatie ongedaan wilt maken? - - - failed to fetch update - ophalen van update mislukt - - - CHECK - CONTROLEER - - - Updates are only downloaded while the car is off. - - - - Current Version - - - - Download - - - - Install Update - - - - INSTALL - - - - Target Branch - - - - SELECT - - - - Select a branch - - - - - SshControl - - SSH Keys - SSH Sleutels - - - 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. - Waarschuwing: dit geeft SSH toegang tot alle openbare sleutels in uw GitHub-instellingen. Voer nooit een andere GitHub-gebruikersnaam in dan die van uzelf. Een medewerker van comma zal u NOOIT vragen om zijn GitHub-gebruikersnaam toe te voegen. - - - ADD - TOEVOEGEN - - - Enter your GitHub username - Voer uw GitHub gebruikersnaam in - - - LOADING - LADEN - - - REMOVE - VERWIJDEREN - - - Username '%1' has no keys on GitHub - Gebruikersnaam '%1' heeft geen SSH sleutels op GitHub - - - Request timed out - Time-out van aanvraag - - - Username '%1' doesn't exist on GitHub - Gebruikersnaam '%1' bestaat niet op GitHub - - - - SshToggle - - Enable SSH - SSH Inschakelen - - - - TermsPage - - Terms & Conditions - Algemene Voorwaarden - - - Decline - Afwijzen - - - Scroll to accept - Scroll om te accepteren - - - Agree - Akkoord - - - - TogglesPanel - - Enable sunnypilot - sunnypilot Inschakelen - - - 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 - Waarschuwingen bij Verlaten Rijstrook Inschakelen - - - 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). - Ontvang waarschuwingen om terug naar de rijstrook te sturen wanneer uw voertuig over een gedetecteerde rijstrookstreep drijft zonder dat de richtingaanwijzer wordt geactiveerd terwijl u harder rijdt dan 50 km/u (31 mph). - - - Use Metric System - Gebruik Metrisch Systeem - - - Display speed in km/h instead of mph. - Geef snelheid weer in km/u in plaats van mph. - - - Record and Upload Driver Camera - Opnemen en Uploaden van de Bestuurders Camera - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - Upload gegevens van de bestuurders camera en help het algoritme voor het monitoren van de bestuurder te verbeteren. - - - Disengage on Accelerator Pedal - Deactiveren Met Gaspedaal - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Indien ingeschakeld, zal het indrukken van het gaspedaal sunnypilot deactiveren. - - - Show ETA in 24h Format - Toon verwachte aankomsttijd in 24-uurs formaat - - - Use 24h format instead of am/pm - Gebruik 24-uurs formaat in plaats van AM en PM - - - Show Map on Left Side of UI - Toon kaart aan linkerkant van het scherm - - - Show map on left side when in split screen view. - Toon kaart links in gesplitste schermweergave. - - - sunnypilot Longitudinal Control - sunnypilot Longitudinale Controle - - - 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 sunnypilot Longitudinal Control - - - - <b>WARNING: sunnypilot longitudinal control is experimental for this car and will disable AEB.</b> - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would. Super experimental. - - - - sunnypilot longitudinal control is not currently available for this car. - - - - Enable experimental longitudinal control to enable this. - - - - - Updater - - Update Required - Update Vereist - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Een update van het besturingssysteem is vereist. Verbind je apparaat met Wi-Fi voor de snelste update-ervaring. De downloadgrootte is ongeveer 1 GB. - - - Connect to Wi-Fi - Maak verbinding met Wi-Fi - - - Install - Installeer - - - Back - Terug - - - Loading... - Aan het laden... - - - Reboot - Opnieuw Opstarten - - - Update failed - Update mislukt - - - - WifiUI - - Scanning for networks... - Scannen naar netwerken... - - - CONNECTING... - VERBINDEN... - - - FORGET - VERGETEN - - - Forget Wi-Fi Network "%1"? - Vergeet Wi-Fi Netwerk "%1"? - - - diff --git a/selfdrive/ui/translations/main_pl.ts b/selfdrive/ui/translations/main_pl.ts deleted file mode 100644 index 6e9170d298..0000000000 --- a/selfdrive/ui/translations/main_pl.ts +++ /dev/null @@ -1,1124 +0,0 @@ - - - - - AbstractAlert - - Close - Zamknij - - - Snooze Update - Zaktualizuj później - - - Reboot and Update - Uruchom ponownie i zaktualizuj - - - - AdvancedNetworking - - Back - Wróć - - - Enable Tethering - Włącz hotspot osobisty - - - Tethering Password - Hasło do hotspotu - - - EDIT - EDYTUJ - - - Enter new tethering password - Wprowadź nowe hasło do hotspotu - - - IP Address - Adres IP - - - Enable Roaming - Włącz roaming danych - - - APN Setting - Ustawienia APN - - - Enter APN - Wprowadź APN - - - leave blank for automatic configuration - Pozostaw puste, aby użyć domyślnej konfiguracji - - - Cellular Metered - - - - Prevent large data uploads when on a metered connection - - - - - AnnotatedCameraWidget - - km/h - km/h - - - mph - mph - - - MAX - MAX - - - SPEED - PRĘDKOŚĆ - - - LIMIT - OGRANICZENIE - - - - ConfirmationDialog - - Ok - Ok - - - Cancel - Anuluj - - - - DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Aby korzystać z sunnypilota musisz zaakceptować regulamin. - - - Back - Wróć - - - Decline, uninstall %1 - Odrzuć, odinstaluj %1 - - - - DevicePanel - - Dongle ID - ID adaptera - - - N/A - N/A - - - Serial - Numer seryjny - - - Driver Camera - Kamera kierowcy - - - PREVIEW - PODGLĄD - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - Wyświetl podgląd z kamery skierowanej na kierowcę, aby upewnić się, że monitoring kierowcy ma dobry zakres widzenia. (pojazd musi być wyłączony) - - - Reset Calibration - Zresetuj kalibrację - - - RESET - ZRESETUJ - - - Are you sure you want to reset calibration? - Czy na pewno chcesz zresetować kalibrację? - - - Review Training Guide - Zapoznaj się z samouczkiem - - - REVIEW - ZAPOZNAJ SIĘ - - - 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? - Czy na pewno chcesz się zapoznać z samouczkiem? - - - Regulatory - Regulacja - - - VIEW - WIDOK - - - Change Language - Zmień język - - - CHANGE - ZMIEŃ - - - Select a language - Wybierz język - - - Reboot - Uruchom ponownie - - - Power Off - Wyłącz - - - 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. - Twoje urządzenie jest skierowane %1° %2 oraz %3° %4. - - - down - w dół - - - up - w górę - - - left - w lewo - - - right - w prawo - - - Are you sure you want to reboot? - Czy na pewno chcesz uruchomić ponownie urządzenie? - - - Disengage to Reboot - Aby uruchomić ponownie, odłącz sterowanie - - - Are you sure you want to power off? - Czy na pewno chcesz wyłączyć urządzenie? - - - Disengage to Power Off - Aby wyłączyć urządzenie, odłącz sterowanie - - - - DriveStats - - Drives - Przejazdy - - - Hours - Godziny - - - ALL TIME - CAŁKOWICIE - - - PAST WEEK - OSTATNI TYDZIEŃ - - - KM - KM - - - Miles - Mile - - - - DriverViewScene - - camera starting - uruchamianie kamery - - - - InputDialog - - Cancel - Anuluj - - - Need at least %n character(s)! - - Wpisana wartość powinna składać się przynajmniej z %n znaku! - Wpisana wartość powinna skłądać się przynajmniej z %n znaków! - Wpisana wartość powinna skłądać się przynajmniej z %n znaków! - - - - - Installer - - Installing... - Instalowanie... - - - Receiving objects: - Odbieranie obiektów: - - - Resolving deltas: - Rozwiązywanie różnic: - - - Updating files: - Aktualizacja plików: - - - - MapETA - - eta - przewidywany czas - - - min - min - - - hr - godz - - - km - km - - - mi - mi - - - - MapInstructions - - km - km - - - m - m - - - mi - mi - - - ft - ft - - - - MapPanel - - Current Destination - Miejsce docelowe - - - CLEAR - WYCZYŚĆ - - - Recent Destinations - Ostatnie miejsca docelowe - - - Try the Navigation Beta - Wypróbuj nawigację w wersji beta - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - Odblokuj nawigację zakręt po zakęcie i wiele więcej subskrybując -comma prime. Zarejestruj się teraz: https://connect.comma.ai - - - No home -location set - Lokalizacja domu -nie została ustawiona - - - No work -location set - Miejsce pracy -nie zostało ustawione - - - no recent destinations - brak ostatnich miejsc docelowych - - - - MapWindow - - Map Loading - Ładowanie Mapy - - - Waiting for GPS - Oczekiwanie na sygnał GPS - - - - MultiOptionDialog - - Select - Wybierz - - - Cancel - Anuluj - - - - Networking - - Advanced - Zaawansowane - - - Enter password - Wprowadź hasło - - - for "%1" - do "%1" - - - Wrong password - Niepoprawne hasło - - - - OffroadHome - - UPDATE - UAKTUALNIJ - - - ALERTS - ALERTY - - - ALERT - ALERT - - - - PairingPopup - - Pair your device to your comma account - Sparuj swoje urzadzenie ze swoim kontem comma - - - Go to https://connect.comma.ai on your phone - Wejdź na stronę https://connect.comma.ai na swoim telefonie - - - Click "add new device" and scan the QR code on the right - Kliknij "add new device" i zeskanuj kod QR znajdujący się po prawej stronie - - - Bookmark connect.comma.ai to your home screen to use it like an app - Dodaj connect.comma.ai do zakładek na swoim ekranie początkowym, aby korzystać z niej jak z aplikacji - - - - PrimeAdWidget - - Upgrade Now - Uaktualnij teraz - - - Become a comma prime member at connect.comma.ai - Zostań członkiem comma prime na connect.comma.ai - - - PRIME FEATURES: - FUNKCJE PRIME: - - - Remote access - Zdalny dostęp - - - 1 year of storage - 1 rok przechowywania danych - - - Developer perks - Udogodnienia dla programistów - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ ZASUBSKRYBOWANO - - - comma prime - comma prime - - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - COMMA POINTS - - - - QObject - - Reboot - Uruchom Ponownie - - - Exit - Wyjdź - - - dashcam - wideorejestrator - - - sunnypilot - sunnypilot - - - %n minute(s) ago - - %n minutę temu - %n minuty temu - %n minut temu - - - - %n hour(s) ago - - % godzinę temu - %n godziny temu - %n godzin temu - - - - %n day(s) ago - - %n dzień temu - %n dni temu - %n dni temu - - - - - Reset - - Reset failed. Reboot to try again. - Wymazywanie zakończone niepowodzeniem. Aby spróbować ponownie, uruchom ponownie urządzenie. - - - Are you sure you want to reset your device? - Czy na pewno chcesz wymazać urządzenie? - - - Resetting device... - Wymazywanie urządzenia... - - - System Reset - Przywróć do ustawień fabrycznych - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Przywracanie do ustawień fabrycznych. Wciśnij potwierdź, aby usunąć wszystkie dane oraz ustawienia. Wciśnij anuluj, aby wznowić uruchamianie. - - - Cancel - Anuluj - - - Reboot - Uruchom ponownie - - - Confirm - Potwiedź - - - Unable to mount data partition. Press confirm to reset your device. - Partycja nie została zamontowana poprawnie. Wciśnij potwierdź, aby uruchomić ponownie urządzenie. - - - - RichTextDialog - - Ok - Ok - - - - SettingsWindow - - × - x - - - Device - Urządzenie - - - Network - Sieć - - - Toggles - Przełączniki - - - Software - Oprogramowanie - - - Navigation - Nawigacja - - - - Setup - - WARNING: Low Voltage - OSTRZEŻENIE: Niskie Napięcie - - - Power your device in a car with a harness or proceed at your own risk. - Podłącz swoje urządzenie do zasilania poprzez podłączenienie go do pojazdu lub kontynuuj na własną odpowiedzialność. - - - Power off - Wyłącz - - - Continue - Kontynuuj - - - Getting Started - Zacznij - - - Before we get on the road, let’s finish installation and cover some details. - Zanim ruszysz w drogę, dokończ instalację i podaj kilka szczegółów. - - - Connect to Wi-Fi - Połącz z Wi-Fi - - - Back - Wróć - - - Continue without Wi-Fi - Kontynuuj bez połączenia z Wif-Fi - - - Waiting for internet - Oczekiwanie na połączenie sieciowe - - - Choose Software to Install - Wybierz oprogramowanie do instalacji - - - Dashcam - Wideorejestrator - - - Custom Software - Własne oprogramowanie - - - Enter URL - Wprowadź adres URL - - - for Custom Software - do własnego oprogramowania - - - Downloading... - Pobieranie... - - - Download Failed - Pobieranie nie powiodło się - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Upewnij się, że wpisany adres URL jest poprawny, a połączenie internetowe działa poprawnie. - - - Reboot device - Uruchom ponownie - - - Start over - Zacznij od początku - - - - SetupWidget - - Finish Setup - Zakończ konfigurację - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Sparuj swoje urządzenie z comma connect (connect.comma.ai) i wybierz swoją ofertę comma prime. - - - Pair device - Sparuj urządzenie - - - - Sidebar - - CONNECT - POŁĄCZENIE - - - OFFLINE - OFFLINE - - - ONLINE - ONLINE - - - ERROR - BŁĄD - - - TEMP - TEMP - - - HIGH - WYSOKA - - - GOOD - DOBRA - - - OK - OK - - - VEHICLE - POJAZD - - - NO - BRAK - - - PANDA - PANDA - - - GPS - GPS - - - SEARCH - SZUKAJ - - - -- - -- - - - Wi-Fi - Wi-FI - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SoftwarePanel - - Git Branch - Gałąź Git - - - Git Commit - Git commit - - - OS Version - Wersja systemu - - - Version - Wersja - - - Last Update Check - Ostatnie sprawdzenie aktualizacji - - - 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 - Sprawdź uaktualnienia - - - CHECKING - SPRAWDZANIE - - - Switch Branch - Zmień gąłąź - - - ENTER - WPROWADŹ - - - The new branch will be pulled the next time the updater runs. - Nowa gałąź będzie pobrana przy następnym uruchomieniu aktualizatora. - - - Enter branch name - Wprowadź nazwę gałęzi - - - Uninstall %1 - Odinstaluj %1 - - - UNINSTALL - ODINSTALUJ - - - Are you sure you want to uninstall? - Czy na pewno chcesz odinstalować? - - - failed to fetch update - pobieranie aktualizacji zakończone niepowodzeniem - - - CHECK - SPRAWDŹ - - - Updates are only downloaded while the car is off. - - - - Current Version - - - - Download - - - - Install Update - - - - INSTALL - - - - Target Branch - - - - SELECT - - - - Select a branch - - - - - SshControl - - SSH Keys - Klucze SSH - - - 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. - Ostrzeżenie: To spowoduje przekazanie dostępu do wszystkich Twoich publicznych kuczy z ustawień GitHuba. Nigdy nie wprowadzaj nazwy użytkownika innej niż swoja. Pracownik comma NIGDY nie poprosi o dodanie swojej nazwy uzytkownika. - - - ADD - DODAJ - - - Enter your GitHub username - Wpisz swoją nazwę użytkownika GitHub - - - LOADING - ŁADOWANIE - - - REMOVE - USUŃ - - - Username '%1' has no keys on GitHub - Użytkownik '%1' nie posiada żadnych kluczy na GitHubie - - - Request timed out - Limit czasu rządania - - - Username '%1' doesn't exist on GitHub - Użytkownik '%1' nie istnieje na GitHubie - - - - SshToggle - - Enable SSH - Włącz SSH - - - - TermsPage - - Terms & Conditions - Regulamin - - - Decline - Odrzuć - - - Scroll to accept - Przewiń w dół, aby zaakceptować - - - Agree - Zaakceptuj - - - - TogglesPanel - - Enable sunnypilot - Włącz sunnypilota - - - 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 - Włącz ostrzeganie przed zmianą pasa ruchu - - - 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). - Otrzymuj alerty o powrocie na właściwy pas, kiedy Twój pojazd przekroczy linię bez włączonego kierunkowskazu jadąc powyżej 50 km/h (31 mph). - - - Use Metric System - Korzystaj z systemu metrycznego - - - Display speed in km/h instead of mph. - Wyświetl prędkość w km/h zamiast mph. - - - Record and Upload Driver Camera - Nagraj i prześlij nagranie z kamery kierowcy - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - Prześlij dane z kamery skierowanej na kierowcę i pomóż poprawiać algorytm monitorowania kierowcy. - - - Disengage on Accelerator Pedal - Odłącz poprzez naciśnięcie gazu - - - 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 - Pokaż oczekiwany czas dojazdu w formacie 24-godzinnym - - - Use 24h format instead of am/pm - Korzystaj z formatu 24-godzinnego zamiast 12-godzinnego - - - Show Map on Left Side of UI - Pokaż mapę po lewej stronie ekranu - - - Show map on left side when in split screen view. - Pokaż mapę po lewej stronie kiedy ekran jest podzielony. - - - sunnypilot Longitudinal Control - Kontrola wzdłużna sunnypilota - - - 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 sunnypilot Longitudinal Control - - - - <b>WARNING: sunnypilot longitudinal control is experimental for this car and will disable AEB.</b> - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would. Super experimental. - - - - sunnypilot longitudinal control is not currently available for this car. - - - - Enable experimental longitudinal control to enable this. - - - - - Updater - - Update Required - Wymagana Aktualizacja - - - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Wymagana aktualizacja systemu operacyjnego. Aby przyspieszyć proces aktualizacji połącz swoje urzeądzenie do Wi-Fi. Rozmiar pobieranej paczki wynosi około 1GB. - - - Connect to Wi-Fi - Połącz się z Wi-Fi - - - Install - Zainstaluj - - - Back - Wróć - - - Loading... - Ładowanie... - - - Reboot - Uruchom ponownie - - - Update failed - Aktualizacja nie powiodła się - - - - WifiUI - - Scanning for networks... - Wyszukiwanie sieci... - - - CONNECTING... - ŁĄCZENIE... - - - FORGET - ZAPOMNIJ - - - Forget Wi-Fi Network "%1"? - Czy chcesz zapomnieć sieć "%1"? - - - diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts deleted file mode 100644 index e820f472af..0000000000 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ /dev/null @@ -1,2853 +0,0 @@ - - - - - AbstractAlert - - Close - Fechar - - - Reboot and Update - Reiniciar e Atualizar - - - - AdvancedNetworking - - Back - Voltar - - - Enable Tethering - Ativar Tether - - - Tethering Password - Senha Tethering - - - EDIT - EDITAR - - - Enter new tethering password - Insira nova senha tethering - - - IP Address - Endereço IP - - - Enable Roaming - Ativar Roaming - - - APN Setting - APN Config - - - Enter APN - Insira APN - - - leave blank for automatic configuration - deixe em branco para configuração automática - - - Cellular Metered - Plano de Dados Limitado - - - Hidden Network - Rede Oculta - - - CONNECT - CONECTE - - - Enter SSID - Digite o SSID - - - Enter password - Insira a senha - - - for "%1" - para "%1" - - - Prevent large data uploads when on a metered cellular connection - Previna o envio de grandes volumes de dados em conexões de celular com franquia de limite de dados - - - default - padrão - - - metered - limitada - - - unmetered - ilimitada - - - Wi-Fi Network Metered - Rede Wi-Fi com Franquia - - - Prevent large data uploads when on a metered Wi-Fi connection - Previna o envio de grandes volumes de dados em conexões Wi-Fi com franquia de limite de dados - - - - 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 - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - OK - - - Cancel - Cancelar - - - - DeclinePage - - Back - Voltar - - - Decline, uninstall %1 - Rejeitar, desintalar %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - Modo Joystick Debug - - - Longitudinal Maneuver Mode - Modo Longitudinal Maneuver - - - openpilot Longitudinal Control (Alpha) - Controle Longitudinal openpilot (Embrionário) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - AVISO: o controle longitudinal openpilot está em estado embrionário para este carro e desativará a Frenagem Automática de Emergência (AEB). - - - Enable ADB - Habilitar ADB - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB (Android Debug Bridge) permite conectar ao seu dispositivo por meio do USB ou através da rede. Veja https://docs.comma.ai/how-to/connect-to-comma para maiores informações. - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - VER - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - Dongle ID - - - N/A - N/A - - - Serial - Serial - - - Driver Camera - Câmera do Motorista - - - PREVIEW - VER - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - Pré-visualizar a câmera voltada para o motorista para garantir que o monitoramento do sistema tenha uma boa visibilidade (veículo precisa estar desligado) - - - Reset Calibration - Reinicializar Calibragem - - - RESET - RESET - - - Are you sure you want to reset calibration? - Tem certeza que quer resetar a calibragem? - - - Review Training Guide - Revisar Guia de Treinamento - - - REVIEW - REVISAR - - - Are you sure you want to review the training guide? - Tem certeza que quer rever o treinamento? - - - Regulatory - Regulatório - - - VIEW - VER - - - Change Language - Alterar Idioma - - - CHANGE - ALTERAR - - - Select a language - Selecione o Idioma - - - Reboot - Reiniciar - - - Power Off - Desligar - - - Your device is pointed %1° %2 and %3° %4. - Seu dispositivo está montado %1° %2 e %3° %4. - - - down - baixo - - - up - cima - - - left - esquerda - - - right - direita - - - Are you sure you want to reboot? - Tem certeza que quer reiniciar? - - - Disengage to Reboot - Desacione para Reiniciar - - - Are you sure you want to power off? - Tem certeza que quer desligar? - - - Disengage to Power Off - Desacione para Desligar - - - Reset - Resetar - - - Review - Revisar - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Pareie seu dispositivo com comma connect (connect.comma.ai) e reivindique sua oferta de comma prime. - - - Pair Device - Parear Dispositivo - - - PAIR - PAREAR - - - Disengage to Reset Calibration - Desacione para Resetar a Calibração - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - O openpilot está em constante calibração, raramente sendo necessário redefini-lo. Redefinir a calibração reiniciará o openpilot se o carro estiver ligado. - - - - -Steering lag calibration is %1% complete. - - -A calibração do atraso da direção está %1% concluída. - - - - -Steering lag calibration is complete. - - -A calibração do atraso da direção foi concluída. - - - Steering torque response calibration is %1% complete. - A calibração da resposta de torque da direção está %1% concluída. - - - Steering torque response calibration is complete. - A calibração da resposta do torque da direção foi concluída. - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - Regulatório - - - Language - - - - Reset Settings - - - - 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 - - - Wake-Up Behavior - - - - Reboot - Reiniciar - - - Power Off - Desligar - - - 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 - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - câmera iniciando - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - MODO EXPERIMENTAL ON - - - CHILL MODE ON - MODO CHILL ON - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - VERIFICAR - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - Modo Firehose: ATIVO - - - ACTIVE - ATIVO - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>%n segmento</b> da sua direção está no conjunto de dados de treinamento até agora. - <b>%n segmentos</b> da sua direção estão no conjunto de dados de treinamento até agora. - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INATIVO</span>: conecte-se a uma rede sem limite <br> de dados - - - Firehose Mode - Modo Firehose - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - LIMITE - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - LIMITE - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - Cancelar - - - Need at least %n character(s)! - - Necessita no mínimo %n caractere! - Necessita no mínimo %n caracteres! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - SELECIONE - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Reinicializar Calibragem - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - Selecione - - - Cancel - Cancelar - - - - Networking - - Advanced - Avançado - - - Enter password - Insira a senha - - - for "%1" - para "%1" - - - Wrong password - Senha incorreta - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Unable to download updates -%1 - Não é possível baixar atualizações -%1 - - - Taking camera snapshots. System won't start until finished. - Tirando fotos da câmera. O sistema não será iniciado até terminar. - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - Uma atualização para o sistema operacional do seu dispositivo está sendo baixada em segundo plano. Você será solicitado a atualizar quando estiver pronto para instalar. - - - Device 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 - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - O dispositivo não conseguiu se registrar no backend da comma.ai. Ele não se conecta nem faz upload para os servidores da comma.ai e não recebe suporte da comma.ai. Se este for um dispositivo adquirido em comma.ai/shop, abra um ticket em https://comma.ai/support. - - - Acknowledge Excessive Actuation - Reconhecer Atuação Excessiva - - - Snooze Update - Adiar Atualização - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - ATUALIZAÇÃO - - - ALERTS - ALERTAS - - - ALERT - ALERTA - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - ASSUMA IMEDIATAMENTE - - - Reboot Device - Reinicie o Dispositivo - - - Waiting to start - Aguardando para iniciar - - - System Unresponsive - Sistema sem Resposta - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - VERIFICAR - - - Country - - - - SELECT - SELECIONE - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - ATUALIZAÇÃO - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - Pareie seu dispositivo à sua conta comma - - - Go to https://connect.comma.ai on your phone - navegue até https://connect.comma.ai no seu telefone - - - Click "add new device" and scan the QR code on the right - Clique "add new device" e escaneie o QR code a seguir - - - Bookmark connect.comma.ai to your home screen to use it like an app - Salve connect.comma.ai como sua página inicial para utilizar como um app - - - Please connect to Wi-Fi to complete initial pairing - Por favor conecte ao Wi-Fi para completar o pareamento inicial - - - - ParamControl - - Cancel - Cancelar - - - Enable - Ativar - - - - ParamControlSP - - Enable - Ativar - - - Cancel - Cancelar - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - REMOVER - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - Cancelar - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - Atualizar Agora - - - Become a comma prime member at connect.comma.ai - Seja um membro comma prime em connect.comma.ai - - - PRIME FEATURES: - BENEFÍCIOS PRIME: - - - Remote access - Acesso remoto (proxy comma) - - - 24/7 LTE connectivity - Conectividade LTE (só nos EUA) - - - 1 year of drive storage - 1 ano de dados em nuvem - - - Remote snapshots - Captura remota - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ INSCRITO - - - comma prime - comma prime - - - - QObject - - %n minute(s) ago - - há %n minuto - há %n minutos - - - - %n hour(s) ago - - há %n hora - há %n horas - - - - %n day(s) ago - - há %n dia - há %n dias - - - - now - agora - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - Dispositivo - - - Network - Rede - - - Toggles - Ajustes - - - Software - Software - - - Developer - Desenvdor - - - Firehose - Firehose - - - - SettingsWindowSP - - × - × - - - Device - Dispositivo - - - Network - Rede - - - sunnylink - - - - Toggles - Ajustes - - - Software - Software - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - Firehose - - - Developer - Desenvdor - - - Display - - - - - SetupWidget - - Finish Setup - Concluir - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Pareie seu dispositivo com comma connect (connect.comma.ai) e reivindique sua oferta de comma prime. - - - Pair device - Parear dispositivo - - - - Sidebar - - CONNECT - CONEXÃO - - - OFFLINE - OFFLINE - - - ONLINE - ONLINE - - - ERROR - ERRO - - - TEMP - TEMP - - - HIGH - ALTA - - - GOOD - BOA - - - OK - OK - - - VEHICLE - VEÍCULO - - - NO - SEM - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - OFFLINE - - - REGIST... - - - - ONLINE - ONLINE - - - ERROR - ERRO - - - SUNNYLINK - - - - - SoftwarePanel - - Updates are only downloaded while the car is off. - Atualizações baixadas durante o motor desligado. - - - Current Version - Versão Atual - - - Download - Download - - - Install Update - Instalar Atualização - - - INSTALL - INSTALAR - - - Target Branch - Alterar Branch - - - SELECT - SELECIONE - - - Select a branch - Selecione uma branch - - - UNINSTALL - REMOVER - - - Uninstall %1 - Desinstalar o %1 - - - Are you sure you want to uninstall? - Tem certeza que quer desinstalar? - - - CHECK - VERIFICAR - - - Uninstall - Desinstalar - - - failed to check for update - falha ao verificar por atualizações - - - up to date, last checked %1 - atualizado, última verificação %1 - - - DOWNLOAD - BAIXAR - - - update available - atualização disponível - - - never - nunca - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - Selecione uma branch - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - Reiniciar - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - Voltar - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - Voltar - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - Chave SSH - - - 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. - Aviso: isso concede acesso SSH a todas as chaves públicas nas configurações do GitHub. Nunca insira um nome de usuário do GitHub que não seja o seu. Um funcionário da comma NUNCA pedirá que você adicione seu nome de usuário do GitHub. - - - ADD - ADICIONAR - - - Enter your GitHub username - Insira seu nome de usuário do GitHub - - - LOADING - CARREGANDO - - - REMOVE - REMOVER - - - Username '%1' has no keys on GitHub - Usuário "%1” não possui chaves no GitHub - - - Request timed out - A solicitação expirou - - - Username '%1' doesn't exist on GitHub - Usuário '%1' não existe no GitHub - - - - SshToggle - - Enable SSH - 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 - - - - 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. - - - - N/A - N/A - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - Declinar - - - Agree - Concordo - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - Ativar Avisos de Saída de Faixa - - - 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). - Receba alertas para voltar para a pista se o seu veículo sair da faixa e a seta não tiver sido acionada previamente quando em velocidades superiores a 50 km/h. - - - Use Metric System - Usar Sistema Métrico - - - Display speed in km/h instead of mph. - Exibir velocidade em km/h invés de mph. - - - Record and Upload Driver Camera - Gravar e Upload Câmera Motorista - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - Upload dados da câmera voltada para o motorista e ajude a melhorar o algoritmo de monitoramentor. - - - Disengage on Accelerator Pedal - Desacionar com Pedal do Acelerador - - - Experimental Mode - Modo Experimental - - - New Driving Visualization - Nova Visualização de Condução - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - O modo Experimental está atualmente indisponível para este carro já que o ACC original do carro é usado para controle longitudinal. - - - openpilot longitudinal control may come in a future update. - O controle longitudinal openpilot poderá vir em uma atualização futura. - - - Aggressive - Disputa - - - Standard - Neutro - - - Relaxed - Calmo - - - Driving Personality - Temperamento de Direção - - - End-to-End Longitudinal Control - Controle Longitudinal de Ponta a Ponta - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - A visualização de condução fará a transição para a câmera grande angular voltada para a estrada em baixas velocidades para mostrar melhor algumas curvas. O logotipo do modo Experimental também será mostrado no canto superior direito. - - - Always-On Driver Monitoring - Monitoramento do Motorista Sempre Ativo - - - Changing this setting will restart openpilot if the car is powered on. - Alterar esta configuração fará com que o openpilot reinicie se o carro estiver ligado. - - - Record and Upload Microphone Audio - Gravar e Fazer Upload do Áudio do Microfone - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - Grave e armazene o áudio do microfone enquanto estiver dirigindo. O áudio será incluído ao vídeo dashcam no comma connect. - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - Selecione - - - Cancel - Cancelar - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - Alterar esta configuração fará com que o openpilot reinicie se o carro estiver ligado. - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - WiFiPromptWidget - - 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> - - - - WifiUI - - Scanning for networks... - Procurando redes... - - - CONNECTING... - CONECTANDO... - - - FORGET - ESQUECER - - - Forget Wi-Fi Network "%1"? - Esquecer Rede Wi-Fi "%1"? - - - Forget - Esquecer - - - diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts deleted file mode 100644 index 67a5a5eaeb..0000000000 --- a/selfdrive/ui/translations/main_th.ts +++ /dev/null @@ -1,2844 +0,0 @@ - - - - - AbstractAlert - - Close - ปิด - - - Reboot and Update - รีบูตและอัปเดต - - - - AdvancedNetworking - - Back - ย้อนกลับ - - - Enable Tethering - ปล่อยฮอตสปอต - - - Tethering Password - รหัสผ่านฮอตสปอต - - - EDIT - แก้ไข - - - Enter new tethering password - ป้อนรหัสผ่านฮอตสปอตใหม่ - - - IP Address - หมายเลขไอพี - - - Enable Roaming - เปิดใช้งานโรมมิ่ง - - - APN Setting - ตั้งค่า APN - - - Enter APN - ป้อนค่า APN - - - leave blank for automatic configuration - เว้นว่างเพื่อตั้งค่าอัตโนมัติ - - - Cellular Metered - ลดการส่งข้อมูลผ่านเซลลูล่าร์ - - - Hidden Network - เครือข่ายที่ซ่อนอยู่ - - - CONNECT - เชื่อมต่อ - - - Enter SSID - ป้อนค่า SSID - - - Enter password - ใส่รหัสผ่าน - - - for "%1" - สำหรับ "%1" - - - Prevent large data uploads when on a metered cellular connection - - - - default - - - - metered - - - - unmetered - - - - Wi-Fi Network Metered - - - - Prevent large data uploads when on a metered Wi-Fi connection - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - ตกลง - - - Cancel - ยกเลิก - - - - DeclinePage - - Back - ย้อนกลับ - - - Decline, uninstall %1 - ปฏิเสธ และถอนการติดตั้ง %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - โหมดดีบักจอยสติ๊ก - - - Longitudinal Maneuver Mode - โหมดการควบคุมการเร่ง/เบรค - - - openpilot Longitudinal Control (Alpha) - ระบบควบคุมการเร่ง/เบรคโดย openpilot (Alpha) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - คำเตือน: การควบคุมการเร่ง/เบรคโดย openpilot สำหรับรถคันนี้ยังอยู่ในสถานะ alpha และระบบเบรคฉุกเฉินอัตโนมัติ (AEB) จะถูกปิด - - - Enable ADB - เปิด ADB - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB (Android Debug Bridge) อนุญาตให้เชื่อมต่ออุปกรณ์ของคุณผ่าน USB หรือผ่านเครือข่าย ดูข้อมูลเพิ่มเติมที่ https://docs.comma.ai/how-to/connect-to-comma - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - ดู - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - Dongle ID - - - N/A - ไม่มี - - - Serial - ซีเรียล - - - Driver Camera - กล้องฝั่งคนขับ - - - PREVIEW - แสดงภาพ - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - ดูภาพตัวอย่างกล้องที่หันเข้าหาคนขับเพื่อให้แน่ใจว่าการตรวจสอบคนขับมีทัศนวิสัยที่ดี (รถต้องดับเครื่องยนต์) - - - Reset Calibration - รีเซ็ตการคาลิเบรท - - - RESET - รีเซ็ต - - - Are you sure you want to reset calibration? - คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตการคาลิเบรท? - - - Review Training Guide - ทบทวนคู่มือการใช้งาน - - - REVIEW - ทบทวน - - - Are you sure you want to review the training guide? - คุณแน่ใจหรือไม่ว่าต้องการทบทวนคู่มือการใช้งาน? - - - Regulatory - ระเบียบข้อบังคับ - - - VIEW - ดู - - - Change Language - เปลี่ยนภาษา - - - CHANGE - เปลี่ยน - - - Select a language - เลือกภาษา - - - Reboot - รีบูต - - - Power Off - ปิดเครื่อง - - - Your device is pointed %1° %2 and %3° %4. - อุปกรณ์ของคุณเอียงไปทาง %2 %1° และ %4 %3° - - - down - ด้านล่าง - - - up - ด้านบน - - - left - ด้านซ้าย - - - right - ด้านขวา - - - Are you sure you want to reboot? - คุณแน่ใจหรือไม่ว่าต้องการรีบูต? - - - Disengage to Reboot - ยกเลิกระบบช่วยขับเพื่อรีบูต - - - Are you sure you want to power off? - คุณแน่ใจหรือไม่ว่าต้องการปิดเครื่อง? - - - Disengage to Power Off - ยกเลิกระบบช่วยขับเพื่อปิดเครื่อง - - - Reset - รีเซ็ต - - - Review - ทบทวน - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - จับคู่อุปกรณ์ของคุณกับ comma connect (connect.comma.ai) และรับข้อเสนอ comma prime ของคุณ - - - Pair Device - จับคู่อุปกรณ์ - - - PAIR - จับคู่ - - - Disengage to Reset Calibration - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - ระเบียบข้อบังคับ - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - คุณแน่ใจหรือไม่ว่าต้องการทบทวนคู่มือการใช้งาน? - - - Review - ทบทวน - - - Select a language - เลือกภาษา - - - Wake-Up Behavior - - - - Reboot - รีบูต - - - Power Off - ปิดเครื่อง - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - Are you sure you want to enter Always Offroad mode? - - - - Disengage to Enter Always Offroad Mode - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - กำลังเปิดกล้อง - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - คุณกำลังใช้โหมดทดลอง - - - CHILL MODE ON - คุณกำลังใช้โหมดชิล - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - ตรวจสอบ - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - โหมดสายยางดับเพลิง: เปิดใช้งาน - - - ACTIVE - เปิดใช้งาน - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - มีการขับขี่ของคุณ <b>%n เซกเมนต์</b> อยู่ในชุดข้อมูลการฝึกฝนแล้วในขณะนี้ - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>ไม่เปิดใช้งาน</span>: เชื่อมต่อกับเครือข่ายที่ไม่จำกัดข้อมูล - - - Firehose Mode - - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - กม./ชม. - - - mph - ไมล์/ชม. - - - MAX - สูงสุด - - - - HudRendererSP - - km/h - กม./ชม. - - - mph - ไมล์/ชม. - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - สูงสุด - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - ยกเลิก - - - Need at least %n character(s)! - - ต้องการอย่างน้อย %n ตัวอักษร! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - เลือก - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - รีเซ็ตการคาลิเบรท - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - ดำเนินการต่อ - - - on Metered - - - - Cancel - ยกเลิก - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - เลือก - - - Cancel - ยกเลิก - - - - Networking - - Advanced - ขั้นสูง - - - Enter password - ใส่รหัสผ่าน - - - for "%1" - สำหรับ "%1" - - - Wrong password - รหัสผ่านผิด - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - อุณหภูมิของอุปกรณ์สูงเกินไป ระบบกำลังทำความเย็นก่อนเริ่ม อุณหภูมิของชิ้นส่วนภายในปัจจุบัน: %1 - - - Unable to download updates -%1 - ไม่สามารถดาวน์โหลดอัพเดทได้ -%1 - - - Taking camera snapshots. System won't start until finished. - กล้องกำลังถ่ายภาพ ระบบจะไม่เริ่มทำงานจนกว่าจะเสร็จ - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - กำลังดาวน์โหลดอัปเดทสำหรับระบบปฏิบัติการอยู่เบื้องหลัง คุณจะได้รับการแจ้งเตือนเมื่อระบบพร้อมสำหรับการติดตั้ง - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - - - - Acknowledge Excessive Actuation - - - - Snooze Update - เลื่อนการอัปเดต - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - อัปเดต - - - ALERTS - การแจ้งเตือน - - - ALERT - การแจ้งเตือน - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - เข้าควบคุมรถเดี๋ยวนี้ - - - Reboot Device - รีบูตอุปกรณ์ - - - Waiting to start - รอเริ่มทำงาน - - - System Unresponsive - ระบบไม่ตอบสนอง - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - ตรวจสอบ - - - Country - - - - SELECT - เลือก - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - อัปเดต - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - จับคู่อุปกรณ์ของคุณกับบัญชี comma ของคุณ - - - Go to https://connect.comma.ai on your phone - ไปที่ https://connect.comma.ai ด้วยโทรศัพท์ของคุณ - - - Click "add new device" and scan the QR code on the right - กดที่ "add new device" และสแกนคิวอาร์โค้ดทางด้านขวา - - - Bookmark connect.comma.ai to your home screen to use it like an app - จดจำ connect.comma.ai โดยการเพิ่มไปยังหน้าจอโฮม เพื่อใช้งานเหมือนเป็นแอปพลิเคชัน - - - Please connect to Wi-Fi to complete initial pairing - กรุณาเชื่อมต่อ Wi-Fi เพื่อทำการจับคู่ครั้งแรกให้เสร็จสิ้น - - - - ParamControl - - Enable - เปิดใช้งาน - - - Cancel - ยกเลิก - - - - ParamControlSP - - Enable - เปิดใช้งาน - - - Cancel - ยกเลิก - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - ลบ - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - ยกเลิก - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - อัพเกรดเดี๋ยวนี้ - - - Become a comma prime member at connect.comma.ai - สมัครสมาชิก comma prime ได้ที่ connect.comma.ai - - - PRIME FEATURES: - คุณสมบัติของ PRIME: - - - Remote access - การเข้าถึงระยะไกล - - - 24/7 LTE connectivity - การเชื่อมต่อ LTE แบบ 24/7 - - - 1 year of drive storage - จัดเก็บข้อมูลการขับขี่นาน 1 ปี - - - Remote snapshots - ภาพถ่ายระยะไกล - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ สมัครสำเร็จ - - - comma prime - comma prime - - - - QObject - - %n minute(s) ago - - %n นาทีที่แล้ว - - - - %n hour(s) ago - - %n ชั่วโมงที่แล้ว - - - - %n day(s) ago - - %n วันที่แล้ว - - - - now - ตอนนี้ - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - อุปกรณ์ - - - Network - เครือข่าย - - - Toggles - ตัวเลือก - - - Software - ซอฟต์แวร์ - - - Developer - นักพัฒนา - - - Firehose - สายยางดับเพลิง - - - - SettingsWindowSP - - × - × - - - Device - อุปกรณ์ - - - Network - เครือข่าย - - - sunnylink - - - - Toggles - ตัวเลือก - - - Software - ซอฟต์แวร์ - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - สายยางดับเพลิง - - - Developer - นักพัฒนา - - - Display - - - - - SetupWidget - - Finish Setup - ตั้งค่าเสร็จสิ้น - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - จับคู่อุปกรณ์ของคุณกับ comma connect (connect.comma.ai) และรับข้อเสนอ comma prime ของคุณ - - - Pair device - จับคู่อุปกรณ์ - - - - Sidebar - - CONNECT - เชื่อมต่อ - - - OFFLINE - ออฟไลน์ - - - ONLINE - ออนไลน์ - - - ERROR - เกิดข้อผิดพลาด - - - TEMP - อุณหภูมิ - - - HIGH - สูง - - - GOOD - ดี - - - OK - พอใช้ - - - VEHICLE - รถยนต์ - - - NO - ไม่พบ - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - ออฟไลน์ - - - REGIST... - - - - ONLINE - ออนไลน์ - - - ERROR - เกิดข้อผิดพลาด - - - SUNNYLINK - - - - - SoftwarePanel - - Uninstall %1 - ถอนการติดตั้ง %1 - - - UNINSTALL - ถอนการติดตั้ง - - - Are you sure you want to uninstall? - คุณแน่ใจหรือไม่ว่าต้องการถอนการติดตั้ง? - - - CHECK - ตรวจสอบ - - - Updates are only downloaded while the car is off. - ตัวอัปเดตจะดำเนินการดาวน์โหลดเมื่อรถดับเครื่องยนต์อยู่เท่านั้น - - - Current Version - เวอร์ชั่นปัจจุบัน - - - Download - ดาวน์โหลด - - - Install Update - ติดตั้งตัวอัปเดต - - - INSTALL - ติดตั้ง - - - Target Branch - Branch ที่เลือก - - - SELECT - เลือก - - - Select a branch - เลือก Branch - - - Uninstall - ถอนการติดตั้ง - - - failed to check for update - ไม่สามารถตรวจสอบอัปเดตได้ - - - DOWNLOAD - ดาวน์โหลด - - - update available - มีอัปเดตใหม่ - - - never - ไม่เคย - - - up to date, last checked %1 - ล่าสุดแล้ว ตรวจสอบครั้งสุดท้ายเมื่อ %1 - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - เลือก Branch - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - รีบูต - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - ย้อนกลับ - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - ย้อนกลับ - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - คีย์ SSH - - - 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. - คำเตือน: สิ่งนี้ให้สิทธิ์ SSH เข้าถึงคีย์สาธารณะทั้งหมดใน GitHub ของคุณ อย่าป้อนชื่อผู้ใช้ GitHub อื่นนอกเหนือจากของคุณเอง พนักงาน comma จะไม่ขอให้คุณเพิ่มชื่อผู้ใช้ GitHub ของพวกเขา - - - ADD - เพิ่ม - - - Enter your GitHub username - ป้อนชื่อผู้ใช้ GitHub ของคุณ - - - LOADING - กำลังโหลด - - - REMOVE - ลบ - - - Username '%1' has no keys on GitHub - ชื่อผู้ใช้ '%1' ไม่มีคีย์บน GitHub - - - Request timed out - ตรวจสอบไม่สำเร็จ เนื่องจากใช้เวลามากเกินไป - - - Username '%1' doesn't exist on GitHub - ไม่พบชื่อผู้ใช้ '%1' บน GitHub - - - - SshToggle - - Enable SSH - เปิดใช้งาน SSH - - - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - Enable sunnylink - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - จับคู่ - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - ไม่มี - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - ปฏิเสธ - - - Agree - ยอมรับ - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - เปิดใช้งานการเตือนการออกนอกเลน - - - Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). - รับการแจ้งเตือนให้เลี้ยวกลับเข้าเลนเมื่อรถของคุณตรวจพบการข้ามช่องจราจรโดยไม่เปิดสัญญาณไฟเลี้ยวในขณะขับขี่ที่ความเร็วเกิน 31 ไมล์ต่อชั่วโมง (50 กม./ชม) - - - Use Metric System - ใช้ระบบเมตริก - - - Display speed in km/h instead of mph. - แสดงความเร็วเป็น กม./ชม. แทน ไมล์/ชั่วโมง - - - Record and Upload Driver Camera - บันทึกและอัปโหลดภาพจากกล้องคนขับ - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - อัปโหลดข้อมูลจากกล้องที่หันหน้าไปทางคนขับ และช่วยปรับปรุงอัลกอริธึมการตรวจสอบผู้ขับขี่ - - - Disengage on Accelerator Pedal - ยกเลิกระบบช่วยขับเมื่อเหยียบคันเร่ง - - - Experimental Mode - โหมดทดลอง - - - New Driving Visualization - การแสดงภาพการขับขี่แบบใหม่ - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - ขณะนี้โหมดทดลองไม่สามารถใช้งานได้ในรถคันนี้ เนื่องจากเปิดใช้ระบบควบคุมการเร่ง/เบรคของรถที่ติดตั้งจากโรงงานอยู่ - - - openpilot longitudinal control may come in a future update. - ระบบควบคุมการเร่ง/เบรคโดย openpilot อาจมาในการอัปเดตในอนาคต - - - Aggressive - ดุดัน - - - Standard - มาตรฐาน - - - Relaxed - ผ่อนคลาย - - - Driving Personality - บุคลิกการขับขี่ - - - End-to-End Longitudinal Control - ควบคุมเร่ง/เบรคแบบ End-to-End - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - การแสดงภาพการขับขี่จะเปลี่ยนไปใช้กล้องมุมกว้างที่หันหน้าไปทางถนนเมื่ออยู่ในความเร็วต่ำ เพื่อแสดงภาพการเลี้ยวที่ดีขึ้น โลโก้โหมดการทดลองจะแสดงที่มุมบนขวาด้วย - - - Always-On Driver Monitoring - การเฝ้าระวังผู้ขับขี่ตลอดเวลา - - - Changing this setting will restart openpilot if the car is powered on. - - - - Record and Upload Microphone Audio - - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - เลือก - - - Cancel - ยกเลิก - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - 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> โหมดสายยางดับเพลิง <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - กำลังสแกนหาเครือข่าย... - - - CONNECTING... - กำลังเชื่อมต่อ... - - - FORGET - เลิกใช้ - - - Forget Wi-Fi Network "%1"? - เลิกใช้เครือข่าย Wi-Fi "%1"? - - - Forget - เลิกใช้ - - - diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts deleted file mode 100644 index 923ab69139..0000000000 --- a/selfdrive/ui/translations/main_tr.ts +++ /dev/null @@ -1,2843 +0,0 @@ - - - - - AbstractAlert - - Close - Kapat - - - Reboot and Update - Güncelle ve Yeniden başlat - - - - AdvancedNetworking - - Back - Geri dön - - - Enable Tethering - Kişisel erişim noktasını aç - - - Tethering Password - Kişisel erişim noktasının parolası - - - EDIT - DÜZENLE - - - Enter new tethering password - Erişim noktasına yeni bir sonraki başlatılışında çekilir. parola belirleyin. - - - IP Address - IP Adresi - - - Enable Roaming - Hücresel veri aç - - - APN Setting - APN Ayarları - - - Enter APN - APN Gir - - - leave blank for automatic configuration - otomatik yapılandırma için boş bırakın - - - Cellular Metered - - - - Hidden Network - - - - CONNECT - BAĞLANTI - - - Enter SSID - APN Gir - - - Enter password - Parolayı girin - - - for "%1" - için "%1" - - - Prevent large data uploads when on a metered cellular connection - - - - default - - - - metered - - - - unmetered - - - - Wi-Fi Network Metered - - - - Prevent large data uploads when on a metered Wi-Fi connection - - - - - AutoLaneChangeTimer - - Auto Lane Change by Blinker - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - s - - - - Off - - - - Nudge - - - - Nudgeless - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - Tamam - - - Cancel - Vazgeç - - - - DeclinePage - - Back - Geri - - - Decline, uninstall %1 - Reddet, Kurulumu kaldır. %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - - - - Longitudinal Maneuver Mode - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - Enable ADB - - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - BAK - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - Adaptör ID - - - N/A - N/A - - - Serial - Seri Numara - - - Driver Camera - Sürücü Kamerası - - - PREVIEW - ÖN İZLEME - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - Sürücü kamerasının görüş açısını test etmek için kamerayı önizleyin (Araç kapalı olmalıdır.) - - - Reset Calibration - Kalibrasyonu sıfırla - - - RESET - SIFIRLA - - - Are you sure you want to reset calibration? - Kalibrasyon ayarını sıfırlamak istediğinizden emin misiniz? - - - Review Training Guide - Eğitim kılavuzunu inceleyin - - - REVIEW - GÖZDEN GEÇİR - - - Are you sure you want to review the training guide? - Eğitim kılavuzunu incelemek istediğinizden emin misiniz? - - - Regulatory - Mevzuat - - - VIEW - BAK - - - Change Language - Dili değiştir - - - CHANGE - DEĞİŞTİR - - - Select a language - Dil seçin - - - Reboot - Yeniden başlat - - - Power Off - Sistemi kapat - - - Your device is pointed %1° %2 and %3° %4. - Cihazınız %1° %2 ve %3° %4 yönünde ayarlı - - - down - aşağı - - - up - yukarı - - - left - sol - - - right - sağ - - - Are you sure you want to reboot? - Cihazı Tekrar başlatmak istediğinizden eminmisiniz? - - - Disengage to Reboot - Bağlantıyı kes ve Cihazı Yeniden başlat - - - Are you sure you want to power off? - Cihazı kapatmak istediğizden eminmisiniz? - - - Disengage to Power Off - Bağlantıyı kes ve Cihazı kapat - - - Reset - - - - Review - - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Cihazınızı comma connect (connect.comma.ai) ile eşleştirin ve comma prime aboneliğine göz atın. - - - Pair Device - - - - PAIR - - - - Disengage to Reset Calibration - - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - - - - - -Steering lag calibration is %1% complete. - - - - - -Steering lag calibration is complete. - - - - Steering torque response calibration is %1% complete. - - - - Steering torque response calibration is complete. - - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - Mevzuat - - - Language - - - - Reset Settings - - - - 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 - - - Wake-Up Behavior - - - - Reboot - Yeniden başlat - - - Power Off - Sistemi kapat - - - 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 - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - kamera başlatılıyor - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - - - - CHILL MODE ON - - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - KONTROL ET - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - - - - ACTIVE - - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - - - - Firehose Mode - - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - MAX - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - MAX - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - Kapat - - - Need at least %n character(s)! - - En az %n karakter gerekli! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Kalibrasyonu sıfırla - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - Devam et - - - on Metered - - - - Cancel - - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - Seç - - - Cancel - İptal et - - - - Networking - - Advanced - Gelişmiş Seçenekler - - - Enter password - Parolayı girin - - - for "%1" - için "%1" - - - Wrong password - Yalnış parola - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - - - - Unable to download updates -%1 - - - - Taking camera snapshots. System won't start until finished. - - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - - - - Acknowledge Excessive Actuation - - - - Snooze Update - Güncellemeyi sessize al - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - GÜNCELLE - - - ALERTS - UYARILAR - - - ALERT - UYARI - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - - - - Reboot Device - - - - Waiting to start - - - - System Unresponsive - - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - KONTROL ET - - - Country - - - - SELECT - - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - GÜNCELLE - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - comma.ai hesabınız ile cihazı eşleştirin - - - Go to https://connect.comma.ai on your phone - Telefonuzdan https://connect.comma.ai sitesine gidin - - - Click "add new device" and scan the QR code on the right - Yeni cihaz eklemek için sağdaki QR kodunu okutun - - - Bookmark connect.comma.ai to your home screen to use it like an app - Uygulama gibi kullanmak için connect.comma.ai sitesini yer işaretlerine ekleyin. - - - Please connect to Wi-Fi to complete initial pairing - - - - - ParamControl - - Enable - - - - Cancel - - - - - ParamControlSP - - Enable - - - - Cancel - - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - KALDIR - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - Hemen yükselt - - - Become a comma prime member at connect.comma.ai - connect.comma.ai üzerinden comma prime üyesi olun - - - PRIME FEATURES: - PRIME ABONELİĞİNİN ÖZELLİKLERİ: - - - Remote access - Uzaktan erişim - - - 24/7 LTE connectivity - - - - 1 year of drive storage - - - - Remote snapshots - - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ ABONE - - - comma prime - comma prime - - - - QObject - - %n minute(s) ago - - %n dakika önce - - - - %n hour(s) ago - - %n saat önce - - - - %n day(s) ago - - %n gün önce - - - - now - - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - x - - - Device - Cihaz - - - Network - - - - Toggles - Değiştirme - - - Software - Yazılım - - - Developer - - - - Firehose - - - - - SettingsWindowSP - - × - x - - - Device - Cihaz - - - Network - - - - sunnylink - - - - Toggles - Değiştirme - - - Software - Yazılım - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - - - - Developer - - - - Display - - - - - SetupWidget - - Finish Setup - Kurulumu bitir - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - Cihazınızı comma connect (connect.comma.ai) ile eşleştirin ve comma prime aboneliğine göz atın. - - - Pair device - Cihazı eşleştirme - - - - Sidebar - - CONNECT - BAĞLANTI - - - OFFLINE - ÇEVRİMDIŞI - - - ONLINE - ÇEVRİMİÇİ - - - ERROR - HATA - - - TEMP - SICAKLIK - - - HIGH - YÜKSEK - - - GOOD - İYİ - - - OK - TAMAM - - - VEHICLE - ARAÇ - - - NO - HAYIR - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-FI - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - ÇEVRİMDIŞI - - - REGIST... - - - - ONLINE - ÇEVRİMİÇİ - - - ERROR - HATA - - - SUNNYLINK - - - - - SoftwarePanel - - Uninstall %1 - Kaldır %1 - - - UNINSTALL - KALDIR - - - Are you sure you want to uninstall? - Kaldırmak istediğinden eminmisin? - - - CHECK - KONTROL ET - - - Updates are only downloaded while the car is off. - - - - Current Version - - - - Download - - - - Install Update - - - - INSTALL - - - - Target Branch - - - - SELECT - - - - Select a branch - - - - Uninstall - - - - failed to check for update - - - - DOWNLOAD - - - - update available - - - - never - - - - up to date, last checked %1 - - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - Yeniden başlat - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - SSH Anahtarları - - - 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. - UYARI: Bu, GitHub ayarlarınızdaki tüm ortak anahtarlara SSH erişimi sağlar. Asla kendi kullanıcı adınız dışında bir sonraki başlatılışında çekilir. GitHub kullanıcı adı girmeyin. - - - ADD - EKLE - - - Enter your GitHub username - Github kullanıcı adınızı giriniz - - - LOADING - YÜKLENİYOR - - - REMOVE - KALDIR - - - Username '%1' has no keys on GitHub - Kullanısının '%1' Github erişim anahtarı yok - - - Request timed out - İstek zaman aşımına uğradı - - - Username '%1' doesn't exist on GitHub - Github kullanıcısı %1 bulunamadı - - - - SshToggle - - Enable SSH - 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 - - - - 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. - - - - N/A - N/A - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - Reddet - - - Agree - Kabul et - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - Şerit ihlali uyarı alın - - - 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). - 50 km/s (31 mph) hızın üzerinde sürüş sırasında aracınız dönüş sinyali vermeden algılanan bir sonraki başlatılışında çekilir. şerit çizgisi ihlalinde şeride geri dönmek için uyarılar alın. - - - Use Metric System - Metrik sistemi kullan - - - Display speed in km/h instead of mph. - Hızı mph yerine km/h şeklinde görüntüleyin. - - - Record and Upload Driver Camera - Sürücü kamerasını kayıt et. - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - Sürücüye bakan kamera verisini yükleyin ve Cihazın algoritmasını geliştirmemize yardımcı olun. - - - Experimental Mode - - - - Disengage on Accelerator Pedal - - - - Aggressive - - - - Standard - - - - Relaxed - - - - Driving Personality - - - - End-to-End Longitudinal Control - - - - New Driving Visualization - - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - - - - openpilot longitudinal control may come in a future update. - - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - - - - Always-On Driver Monitoring - - - - Changing this setting will restart openpilot if the car is powered on. - - - - Record and Upload Microphone Audio - - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - Seç - - - Cancel - - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - WiFiPromptWidget - - 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> - - - - - WifiUI - - Scanning for networks... - Ağ aranıyor... - - - CONNECTING... - BAĞLANILIYOR... - - - FORGET - UNUT - - - Forget Wi-Fi Network "%1"? - Wi-Fi ağını unut "%1"? - - - Forget - - - - diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts deleted file mode 100644 index 3be9377503..0000000000 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ /dev/null @@ -1,2848 +0,0 @@ - - - - - AbstractAlert - - Close - 关闭 - - - Reboot and Update - 重启并更新 - - - - AdvancedNetworking - - Back - 返回 - - - Enable Tethering - 启用WiFi热点 - - - Tethering Password - WiFi热点密码 - - - EDIT - 编辑 - - - Enter new tethering password - 输入新的WiFi热点密码 - - - IP Address - IP 地址 - - - Enable Roaming - 启用数据漫游 - - - APN Setting - APN 设置 - - - Enter APN - 输入 APN - - - leave blank for automatic configuration - 留空以自动配置 - - - Cellular Metered - 按流量计费的手机移动网络 - - - Hidden Network - 隐藏的网络 - - - CONNECT - 连线 - - - Enter SSID - 输入 SSID - - - Enter password - 输入密码 - - - for "%1" - 网络名称:"%1" - - - Prevent large data uploads when on a metered cellular connection - 在按流量计费的移动网络上,防止上传大数据 - - - default - 默认 - - - metered - 按流量计费 - - - unmetered - 不按流量计费 - - - Wi-Fi Network Metered - 按流量计费的 WLAN 网络 - - - Prevent large data uploads when on a metered Wi-Fi connection - 在按流量计费的 WLAN 网络上,防止上传大数据 - - - - 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 - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - 好的 - - - Cancel - 取消 - - - - DeclinePage - - Back - 返回 - - - Decline, uninstall %1 - 拒绝并卸载%1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - 摇杆调试模式 - - - Longitudinal Maneuver Mode - 纵向操控测试模式 - - - openpilot Longitudinal Control (Alpha) - openpilot纵向控制(Alpha 版) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 警告:此车辆的 openpilot 纵向控制功能目前处于Alpha版本,使用此功能将会停用自动紧急制动(AEB)功能。 - - - Enable ADB - 启用 ADB - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB(Android调试桥接)允许通过USB或网络连接到您的设备。更多信息请参见 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - 查看 - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - 设备ID(Dongle ID) - - - N/A - N/A - - - Serial - 序列号 - - - Driver Camera - 驾驶员摄像头 - - - PREVIEW - 预览 - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - 打开并预览驾驶员摄像头,以确保驾驶员监控具有良好视野。(仅熄火时可用) - - - Reset Calibration - 重置设备校准 - - - RESET - 重置 - - - Are you sure you want to reset calibration? - 您确定要重置设备校准吗? - - - Review Training Guide - 新手指南 - - - REVIEW - 查看 - - - Are you sure you want to review the training guide? - 您确定要查看新手指南吗? - - - Regulatory - 监管信息 - - - VIEW - 查看 - - - Change Language - 切换语言 - - - CHANGE - 切换 - - - Select a language - 选择语言 - - - Reboot - 重启 - - - Power Off - 关机 - - - Your device is pointed %1° %2 and %3° %4. - 您的设备校准为%1° %2、%3° %4。 - - - down - 朝下 - - - up - 朝上 - - - left - 朝左 - - - right - 朝右 - - - Are you sure you want to reboot? - 您确定要重新启动吗? - - - Disengage to Reboot - 取消openpilot以重新启动 - - - Are you sure you want to power off? - 您确定要关机吗? - - - Disengage to Power Off - 取消openpilot以关机 - - - Reset - 重置 - - - Review - 预览 - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 将您的设备与comma connect (connect.comma.ai)配对并领取您的comma prime优惠。 - - - Pair Device - 配对设备 - - - PAIR - 配对 - - - Disengage to Reset Calibration - 解除以重置校准 - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - openpilot 会持续进行校准,因此很少需要重置。如果车辆电源已开启,重置校准会重新启动 openpilot。 - - - - -Steering lag calibration is %1% complete. - - -转向延迟校准已完成 %1%。 - - - - -Steering lag calibration is complete. - - -转向延迟校准已完成。 - - - Steering torque response calibration is %1% complete. - 转向扭矩响应校准已完成 %1%。 - - - Steering torque response calibration is complete. - 转向扭矩响应校准已完成。 - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - 监管信息 - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - 您确定要查看新手指南吗? - - - Review - 预览 - - - Select a language - 选择语言 - - - Wake-Up Behavior - - - - Reboot - 重启 - - - Power Off - 关机 - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - Are you sure you want to enter Always Offroad mode? - - - - Disengage to Enter Always Offroad Mode - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - 正在启动相机 - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - 试验模式运行 - - - CHILL MODE ON - 轻松模式运行 - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - 查看 - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - Firehose 模式:激活中 - - - ACTIVE - 激活中 - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>目前已有 %n 段</b> 您的驾驶数据被纳入训练数据集。 - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>闲置</span>:请连接到不限流量的网络 - - - Firehose Mode - Firehose 模式 - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - 最高定速 - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - 最高定速 - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - 取消 - - - Need at least %n character(s)! - - 至少需要 %n 个字符! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - 选择 - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重置设备校准 - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - 继续 - - - on Metered - - - - Cancel - 取消 - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - 选择 - - - Cancel - 取消 - - - - Networking - - Advanced - 高级 - - - Enter password - 输入密码 - - - for "%1" - 网络名称:"%1" - - - Wrong password - 密码错误 - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Unable to download updates -%1 - 无法下载更新 -%1 - - - Taking camera snapshots. System won't start until finished. - 正在使用相机拍摄中。在完成之前,系统将无法启动。 - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - 一个针对您设备的操作系统更新正在后台下载中。当更新准备好安装时,您将收到提示进行更新。 - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 设备温度过高。系统正在冷却中,等冷却完毕后才会启动。目前内部组件温度:%1 - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - 设备未能注册到 comma.ai 后端。该设备将无法连接或上传数据到 comma.ai 服务器,也无法获得 comma.ai 的支持。如果该设备是在 comma.ai/shop 购买的,请访问 https://comma.ai/support 提交工单。 - - - Acknowledge Excessive Actuation - - - - Snooze Update - 暂停更新 - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - 更新 - - - ALERTS - 警报 - - - ALERT - 警报 - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - 立即接管 - - - Reboot Device - 重启设备 - - - Waiting to start - 等待开始 - - - System Unresponsive - 系统无响应 - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - 查看 - - - Country - - - - SELECT - 选择 - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - 更新 - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - 将您的设备与comma账号配对 - - - Go to https://connect.comma.ai on your phone - 在手机上访问 https://connect.comma.ai - - - Click "add new device" and scan the QR code on the right - 点击“添加新设备”,扫描右侧二维码 - - - Bookmark connect.comma.ai to your home screen to use it like an app - 将 connect.comma.ai 收藏到您的主屏幕,以便像应用程序一样使用它 - - - Please connect to Wi-Fi to complete initial pairing - 请连接 Wi-Fi 以完成初始配对 - - - - ParamControl - - Cancel - 取消 - - - Enable - 启用 - - - - ParamControlSP - - Enable - 启用 - - - Cancel - 取消 - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - 删除 - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - 取消 - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - 现在升级 - - - Become a comma prime member at connect.comma.ai - 打开connect.comma.ai以注册comma prime会员 - - - PRIME FEATURES: - comma prime特权: - - - Remote access - 远程访问 - - - 24/7 LTE connectivity - 全天候 LTE 连接 - - - 1 year of drive storage - 一年的行驶记录储存空间 - - - Remote snapshots - 远程快照 - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ 已订阅 - - - comma prime - comma prime - - - - QObject - - %n minute(s) ago - - %n 分钟前 - - - - %n hour(s) ago - - %n 小时前 - - - - %n day(s) ago - - %n 天前 - - - - now - 现在 - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - 设备 - - - Network - 网络 - - - Toggles - 设定 - - - Software - 软件 - - - Developer - 开发人员 - - - Firehose - Firehose - - - - SettingsWindowSP - - × - × - - - Device - 设备 - - - Network - 网络 - - - sunnylink - - - - Toggles - 设定 - - - Software - 软件 - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - Firehose - - - Developer - 开发人员 - - - Display - - - - - SetupWidget - - Finish Setup - 完成设置 - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 将您的设备与comma connect (connect.comma.ai)配对并领取您的comma prime优惠。 - - - Pair device - 配对设备 - - - - Sidebar - - CONNECT - CONNECT - - - OFFLINE - 离线 - - - ONLINE - 在线 - - - ERROR - 连接出错 - - - TEMP - 设备温度 - - - HIGH - 过热 - - - GOOD - 良好 - - - OK - 一般 - - - VEHICLE - 车辆连接 - - - NO - - - - PANDA - PANDA - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - 以太网 - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - 离线 - - - REGIST... - - - - ONLINE - 在线 - - - ERROR - 连接出错 - - - SUNNYLINK - - - - - SoftwarePanel - - Updates are only downloaded while the car is off. - 车辆熄火时才能下载升级文件。 - - - Current Version - 当前版本 - - - Download - 下载 - - - Install Update - 安装更新 - - - INSTALL - 安装 - - - Target Branch - 目标分支 - - - SELECT - 选择 - - - Select a branch - 选择分支 - - - UNINSTALL - 卸载 - - - Uninstall %1 - 卸载 %1 - - - Are you sure you want to uninstall? - 您确定要卸载吗? - - - CHECK - 查看 - - - Uninstall - 卸载 - - - failed to check for update - 检查更新失败 - - - up to date, last checked %1 - 已经是最新版本,上次检查时间为 %1 - - - DOWNLOAD - 下载 - - - update available - 有可用的更新 - - - never - 从未更新 - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - 选择分支 - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - 重启 - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - 返回 - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - 返回 - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - SSH密钥 - - - 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. - 警告:这将授予SSH访问权限给您GitHub设置中的所有公钥。切勿输入您自己以外的GitHub用户名。comma员工永远不会要求您添加他们的GitHub用户名。 - - - ADD - 添加 - - - Enter your GitHub username - 输入您的GitHub用户名 - - - LOADING - 正在加载 - - - REMOVE - 删除 - - - Username '%1' has no keys on GitHub - 用户名“%1”在GitHub上没有密钥 - - - Request timed out - 请求超时 - - - Username '%1' doesn't exist on GitHub - GitHub上不存在用户名“%1” - - - - SshToggle - - Enable SSH - 启用SSH - - - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - Enable sunnylink - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - 配对 - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - N/A - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - 拒绝 - - - Agree - 同意 - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - 启用车道偏离警告 - - - Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). - 车速超过31mph(50km/h)时,若检测到车辆越过车道线且未打转向灯,系统将发出警告以提醒您返回车道。 - - - Use Metric System - 使用公制单位 - - - Display speed in km/h instead of mph. - 显示车速时,以km/h代替mph。 - - - Record and Upload Driver Camera - 录制并上传驾驶员摄像头 - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - 上传驾驶员摄像头的数据,帮助改进驾驶员监控算法。 - - - Disengage on Accelerator Pedal - 踩油门时取消控制 - - - Experimental Mode - 测试模式 - - - New Driving Visualization - 新驾驶视角 - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 由于此车辆使用自带的ACC纵向控制,当前无法使用试验模式。 - - - openpilot longitudinal control may come in a future update. - openpilot纵向控制可能会在未来的更新中提供。 - - - Aggressive - 积极 - - - Standard - 标准 - - - Relaxed - 舒适 - - - Driving Personality - 驾驶风格 - - - End-to-End Longitudinal Control - 端到端纵向控制 - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - 在低速时,驾驶可视化将转换为道路朝向的广角摄像头,以更好地展示某些转弯。测试模式标志也将显示在右上角。 - - - Always-On Driver Monitoring - 驾驶员监控常开 - - - Changing this setting will restart openpilot if the car is powered on. - 如果车辆已通电,更改此设置将会重新启动 openpilot。 - - - Record and Upload Microphone Audio - 录制并上传麦克风音频 - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - 在驾驶时录制并存储麦克风音频。该音频将会包含在 comma connect 的行车记录仪视频中。 - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - 选择 - - - Cancel - 取消 - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - 如果车辆已通电,更改此设置将会重新启动 openpilot。 - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - 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 模式 <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - 正在扫描网络…… - - - CONNECTING... - 正在连接…… - - - FORGET - 忽略 - - - Forget Wi-Fi Network "%1"? - 忽略WiFi网络 "%1"? - - - Forget - 忽略 - - - diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts deleted file mode 100644 index fac29bed62..0000000000 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ /dev/null @@ -1,2848 +0,0 @@ - - - - - AbstractAlert - - Close - 關閉 - - - Reboot and Update - 重啟並更新 - - - - AdvancedNetworking - - Back - 回上頁 - - - Enable Tethering - 啟用網路分享 - - - Tethering Password - 網路分享密碼 - - - EDIT - 編輯 - - - Enter new tethering password - 輸入新的網路分享密碼 - - - IP Address - IP 地址 - - - Enable Roaming - 啟用漫遊 - - - APN Setting - APN 設置 - - - Enter APN - 輸入 APN - - - leave blank for automatic configuration - 留空白將自動配置 - - - Cellular Metered - 計費的行動網路 - - - Hidden Network - 隱藏的網路 - - - CONNECT - 連線 - - - Enter SSID - 輸入 SSID - - - Enter password - 輸入密碼 - - - for "%1" - 給 "%1" - - - Prevent large data uploads when on a metered cellular connection - 在使用計費行動網路時,防止上傳大量數據 - - - default - 預設 - - - metered - 計費 - - - unmetered - 非計費 - - - Wi-Fi Network Metered - 計費 Wi-Fi 網路 - - - Prevent large data uploads when on a metered Wi-Fi connection - 在使用計費 Wi-Fi 網路時,防止上傳大量數據 - - - - 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 - - - - - Brightness - - Auto (Dark) - - - - Auto - - - - Global Brightness - - - - Overrides the brightness of the device. This applies to both onroad and offroad screens. - - - - - ConfirmationDialog - - Ok - 確定 - - - Cancel - 取消 - - - - DeclinePage - - Back - 回上頁 - - - Decline, uninstall %1 - 拒絕並解除安裝 %1 - - - You must accept the Terms and Conditions in order to use sunnypilot. - - - - - DeveloperPanel - - Joystick Debug Mode - 搖桿調試模式 - - - Longitudinal Maneuver Mode - 縱向操控測試模式 - - - openpilot Longitudinal Control (Alpha) - openpilot 縱向控制 (Alpha 版) - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 警告:此車輛的 openpilot 縱向控制功能目前處於 Alpha 版本,使用此功能將會停用自動緊急煞車(AEB)功能。 - - - Enable ADB - 啟用 ADB - - - ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB(Android 調試橋接)允許通過 USB 或網絡連接到您的設備。更多信息請參見 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - - - - - DeveloperPanelSP - - Show Advanced Controls - - - - Toggle visibility of advanced sunnypilot controls. -This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. - - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Enable Quickboot Mode - - - - Error Log - - - - VIEW - 觀看 - - - View the error log for sunnypilot crashes. - - - - When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - - - - Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. - - - - Enable Copyparty service - - - - Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP. - - - - - DevicePanel - - Dongle ID - Dongle ID - - - N/A - 無法使用 - - - Serial - 序號 - - - Driver Camera - 駕駛員監控鏡頭 - - - PREVIEW - 預覽 - - - Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - 預覽駕駛員監控鏡頭畫面,以確保其具有良好視野。(僅在熄火時可用) - - - Reset Calibration - 重設校準 - - - RESET - 重設 - - - Are you sure you want to reset calibration? - 您確定要重設校準嗎? - - - Review Training Guide - 觀看使用教學 - - - REVIEW - 觀看 - - - Are you sure you want to review the training guide? - 您確定要觀看使用教學嗎? - - - Regulatory - 法規/監管 - - - VIEW - 觀看 - - - Change Language - 更改語言 - - - CHANGE - 更改 - - - Select a language - 選擇語言 - - - Reboot - 重新啟動 - - - Power Off - 關機 - - - Your device is pointed %1° %2 and %3° %4. - 你的裝置目前朝%2 %1° 以及朝%4 %3° 。 - - - down - - - - up - - - - left - - - - right - - - - Are you sure you want to reboot? - 您確定要重新啟動嗎? - - - Disengage to Reboot - 請先取消控車才能重新啟動 - - - Are you sure you want to power off? - 您確定您要關機嗎? - - - Disengage to Power Off - 請先取消控車才能關機 - - - Reset - 重設 - - - Review - 回顧 - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 將您的裝置與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。 - - - Pair Device - 配對裝置 - - - PAIR - 配對 - - - Disengage to Reset Calibration - 解除以重設校準 - - - openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. - openpilot 會持續進行校準,因此很少需要重設。若車輛電源開啟,重設校準將會重新啟動 openpilot。 - - - - -Steering lag calibration is %1% complete. - - -轉向延遲校準已完成 %1%。 - - - - -Steering lag calibration is complete. - - -轉向延遲校準已完成。 - - - Steering torque response calibration is %1% complete. - 轉向扭矩反應校準已完成 %1%。 - - - Steering torque response calibration is complete. - 轉向扭矩反應校準已完成。 - - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. - - - - - DevicePanelSP - - Quiet Mode - - - - Driver Camera Preview - - - - Training Guide - - - - Regulatory - 法規/監管 - - - Language - - - - Reset Settings - - - - Are you sure you want to review the training guide? - 您確定要觀看使用教學嗎? - - - Review - 回顧 - - - Select a language - 選擇語言 - - - Wake-Up Behavior - - - - Reboot - 重新啟動 - - - Power Off - 關機 - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - Are you sure you want to enter Always Offroad mode? - - - - Disengage to Enter Always Offroad Mode - - - - 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. - - - - Exit Always Offroad - - - - ⁍ Default: Device will boot/wake-up normally & will be ready to engage. - - - - ⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up. - - - - Controls state of the device after boot/sleep. - - - - Onroad Uploads - - - - Enable Always Offroad - - - - - DisplayPanel - - Onroad Screen: Reduced Brightness - - - - Turn off device screen or reduce brightness after driving starts. It automatically brightens again when screen is touched or a visible alert is displayed. - - - - Interactivity Timeout - - - - Apply a custom timeout for settings UI. -This is the time after which settings UI closes automatically if user is not interacting with the screen. - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - KM - - - - Miles - - - - - DriverViewWindow - - camera starting - 開啟相機中 - - - - ExitOffroadButton - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - - - - EXIT ALWAYS OFFROAD MODE - - - - - ExperimentalModeButton - - EXPERIMENTAL MODE ON - 實驗模式 ON - - - CHILL MODE ON - 輕鬆模式 ON - - - - ExternalStorageControl - - External Storage - - - - Extend your comma device's storage by inserting a USB drive into the aux port. - - - - CHECK - 檢查 - - - MOUNT - - - - UNMOUNT - - - - FORMAT - - - - Are you sure you want to format this drive? This will erase all data. - - - - Format - - - - formatting - - - - insert drive - - - - needs format - - - - mounting - - - - unmounting - - - - - FirehosePanel - - Firehose Mode: ACTIVE - Firehose 模式:啟用中 - - - ACTIVE - 啟用中 - - - <b>%n segment(s)</b> of your driving is in the training dataset so far. - - <b>目前已有 %n 段</b> 您的駕駛數據被納入訓練資料集。 - - - - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>閒置中</span>:請連接到不按流量計費的網絡 - - - Firehose Mode - Firehose 模式 - - - sunnypilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - - - - HudRenderer - - km/h - km/h - - - mph - mph - - - MAX - 最高 - - - - HudRendererSP - - km/h - km/h - - - mph - mph - - - GREEN -LIGHT - - - - LEAD VEHICLE -DEPARTING - - - - SPEED - - - - LIMIT - - - - Near - - - - km - - - - m - - - - mi - - - - ft - - - - AHEAD - - - - MAX - 最高 - - - - HyundaiSettings - - Off - - - - Dynamic - - - - Predictive - - - - Custom Longitudinal Tuning - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - - - - Off: Uses default tuning - - - - Dynamic: Adjusts acceleration limits based on current speed - - - - Predictive: Uses future trajectory data to anticipate needed adjustments - - - - Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - - - - - InputDialog - - Cancel - 取消 - - - Need at least %n character(s)! - - 需要至少 %n 個字元! - - - - - 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 - - - - Pause Lateral Control with Blinker - - - - Pause lateral control with blinker when traveling below the desired speed selected. - - - - Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - - - - Start the vehicle to check vehicle compatibility. - - - - This platform supports all MADS settings. - - - - This platform supports limited MADS settings. - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Customize Params - - - - - LongitudinalPanel - - Custom ACC Speed Increments - - - - Enable custom Short & Long press increments for cruise speed increase/decrease. - - - - This feature can only be used with openpilot longitudinal control enabled. - - - - This feature is not supported on this platform due to vehicle limitations. - - - - Start the vehicle to check vehicle compatibility. - - - - Intelligent Cruise Button Management (ICBM) (Alpha) - - - - When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control. - - - - Smart Cruise Control - Vision - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Smart Cruise Control - Map - - - - Use map data to estimate the appropriate speed to drive through turns ahead. - - - - Speed Limit - - - - - MadsSettings - - Toggle with Main Cruise - - - - Unified Engagement Mode (UEM) - - - - Steering Mode on Brake Pedal - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Start the vehicle to check vehicle compatibility. - - - - This feature defaults to OFF, and does not allow selection due to vehicle limitations. - - - - This feature defaults to ON, and does not allow selection due to vehicle limitations. - - - - This platform only supports Disengage mode due to vehicle limitations. - - - - Remain Active - - - - Remain Active: ALC will remain active when the brake pedal is pressed. - - - - Pause - - - - Pause: ALC will pause when the brake pedal is pressed. - - - - Disengage - - - - Disengage: ALC will disengage when the brake pedal is pressed. - - - - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - - - - - MaxTimeOffroad - - Max Time Offroad - - - - Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - - - - Always On - - - - h - - - - m - - - - (default) - - - - - ModelsPanel - - Current Model - - - - SELECT - 選取 - - - Clear Model Cache - - - - CLEAR - - - - Driving Model - - - - Navigation Model - - - - Vision Model - - - - Policy Model - - - - Live Learning Steer Delay - - - - Adjust Software Delay - - - - Adjust the software delay when Live Learning Steer Delay is toggled off. -The default software delay value is 0.2 - - - - %1 - %2 - - - - downloaded - - - - ready - - - - from cache - - - - download failed - %1 - - - - pending - %1 - - - - Fetching models... - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重設校準 - - - Driving Model Selector - - - - This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? - - - - Clear Cache - - - - Warning: You are on a metered connection! - - - - Continue - 繼續 - - - on Metered - - - - Cancel - 取消 - - - Refresh Model List - - - - REFRESH - - - - Fetching Latest Models - - - - 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. - - - - Live Steer Delay: - - - - Actuator Delay: - - - - Software Delay: - - - - Total Delay: - - - - Use Lane Turn Desires - - - - Adjust Lane Turn Speed - - - - Set the maximum speed for lane turn desires. Default is 19 %1. - - - - - MultiOptionDialog - - Select - 選擇 - - - Cancel - 取消 - - - - Networking - - Advanced - 進階 - - - Enter password - 輸入密碼 - - - for "%1" - 給 "%1" - - - Wrong password - 密碼錯誤 - - - - NetworkingSP - - Scan - - - - Scanning... - - - - - NeuralNetworkLateralControl - - Neural Network Lateral Control (NNLC) - - - - NNLC is currently not available on this platform. - - - - Start the car to check car compatibility - - - - NNLC Not Loaded - - - - NNLC Loaded - - - - Match - - - - Exact - - - - Fuzzy - - - - Match: "Exact" is ideal, but "Fuzzy" is fine too. - - - - Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - - - - Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - - - - with feedback, or to provide log data for your car if your car is currently unsupported: - - - - if there are any issues: - - - - and donate logs to get NNLC loaded for your car: - - - - - OffroadAlert - - Unable to download updates -%1 - 無法下載更新 -%1 - - - Taking camera snapshots. System won't start until finished. - 正在使用相機拍攝中。在完成之前,系統將無法啟動。 - - - An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - 一個有關操作系統的更新正在後台下載中。當更新準備好安裝時,您將收到提示進行更新。 - - - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - 裝置溫度過高。系統正在冷卻中,等冷卻完畢後才會啟動。目前內部組件溫度:%1 - - - Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support. - 裝置註冊 comma.ai 後端失敗。此裝置將無法連線或上傳資料至 comma.ai 伺服器,也無法獲得 comma.ai 的支援。若此裝置購自 comma.ai/shop,請至 https://comma.ai/support 建立支援請求。 - - - Acknowledge Excessive Actuation - - - - Snooze Update - 暫停更新 - - - openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. - - - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - - - sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - - - - OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. - -%1 - - - - <b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three. - - - - - OffroadHome - - UPDATE - 更新 - - - ALERTS - 提醒 - - - ALERT - 提醒 - - - - OffroadHomeSP - - ALWAYS OFFROAD ACTIVE - - - - - OnroadAlerts - - TAKE CONTROL IMMEDIATELY - 立即接管 - - - Reboot Device - 請重新啟裝置 - - - Waiting to start - 等待開始 - - - System Unresponsive - 系統無回應 - - - sunnypilot Unavailable - - - - - OsmPanel - - Mapd Version - - - - Offline Maps ETA - - - - Time Elapsed - - - - Downloaded Maps - - - - DELETE - - - - This will delete ALL downloaded maps - -Are you sure you want to delete all the maps? - - - - Yes, delete all the maps. - - - - Database Update - - - - CHECK - 檢查 - - - Country - - - - SELECT - 選取 - - - Fetching Country list... - - - - State - - - - Fetching State list... - - - - All - - - - REFRESH - - - - UPDATE - 更新 - - - Download starting... - - - - Error: Invalid download. Retry. - - - - Download complete! - - - - - -Warning: You are on a metered connection! - - - - This will start the download process and it might take a while to complete. - - - - Continue on Metered - - - - Start Download - - - - m - - - - s - - - - Calculating... - - - - Downloaded - - - - Calculating ETA... - - - - Ready - - - - Time remaining: - - - - - PairingPopup - - Pair your device to your comma account - 將裝置與您的 comma 帳號配對 - - - Go to https://connect.comma.ai on your phone - 用手機連至 https://connect.comma.ai - - - Click "add new device" and scan the QR code on the right - 點選 "add new device" 後掃描右邊的二維碼 - - - Bookmark connect.comma.ai to your home screen to use it like an app - 將 connect.comma.ai 加入您的主螢幕,以便像手機 App 一樣使用它 - - - Please connect to Wi-Fi to complete initial pairing - 請連接 Wi-Fi 以完成初始配對 - - - - ParamControl - - Cancel - 取消 - - - Enable - 啟用 - - - - ParamControlSP - - Enable - 啟用 - - - Cancel - 取消 - - - - PlatformSelector - - Vehicle - - - - SEARCH - - - - Search your vehicle - - - - Enter model year (e.g., 2021) and model name (Toyota Corolla): - - - - SEARCHING - - - - REMOVE - 移除 - - - This setting will take effect immediately. - - - - This setting will take effect once the device enters offroad state. - - - - Vehicle Selector - - - - Confirm - - - - Cancel - 取消 - - - No vehicles found for query: %1 - - - - Select a vehicle - - - - Unrecognized Vehicle - - - - Fingerprinted automatically - - - - Manually selected - - - - Not fingerprinted or manually selected - - - - Select vehicle to force fingerprint manually. - - - - Colors represent fingerprint status: - - - - - PrimeAdWidget - - Upgrade Now - 馬上升級 - - - Become a comma prime member at connect.comma.ai - 成為 connect.comma.ai 的高級會員 - - - PRIME FEATURES: - 高級會員特點: - - - Remote access - 遠端存取 - - - 24/7 LTE connectivity - 24/7 LTE 連線 - - - 1 year of drive storage - 一年的行駛記錄儲存空間 - - - Remote snapshots - 遠端快照 - - - - PrimeUserWidget - - ✓ SUBSCRIBED - ✓ 已訂閱 - - - comma prime - comma 高級會員 - - - - QObject - - %n minute(s) ago - - %n 分鐘前 - - - - %n hour(s) ago - - %n 小時前 - - - - %n day(s) ago - - %n 天前 - - - - now - 現在 - - - sunnypilot - - - - None - - - - Fixed - - - - Percent - - - - Car -Only - - - - Map -Only - - - - Car -First - - - - Map -First - - - - Combined -Data - - - - Off - - - - Information - - - - Warning - - - - Assist - - - - - SettingsWindow - - × - × - - - Device - 裝置 - - - Network - 網路 - - - Toggles - 設定 - - - Software - 軟體 - - - Developer - 開發人員 - - - Firehose - Firehose - - - - SettingsWindowSP - - × - × - - - Device - 裝置 - - - Network - 網路 - - - sunnylink - - - - Toggles - 設定 - - - Software - 軟體 - - - Models - - - - Steering - - - - Cruise - - - - Visuals - - - - OSM - - - - Trips - - - - Vehicle - - - - Firehose - Firehose - - - Developer - 開發人員 - - - Display - - - - - SetupWidget - - Finish Setup - 完成設置 - - - Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 將您的裝置與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。 - - - Pair device - 配對裝置 - - - - Sidebar - - CONNECT - 雲端服務 - - - OFFLINE - 已離線 - - - ONLINE - 已連線 - - - ERROR - 錯誤 - - - TEMP - 溫度 - - - HIGH - 偏高 - - - GOOD - 正常 - - - OK - 一般 - - - VEHICLE - 車輛通訊 - - - NO - 未連線 - - - PANDA - 車輛通訊 - - - -- - -- - - - Wi-Fi - Wi-Fi - - - ETH - ETH - - - 2G - 2G - - - 3G - 3G - - - LTE - LTE - - - 5G - 5G - - - - SidebarSP - - DISABLED - - - - OFFLINE - 已離線 - - - REGIST... - - - - ONLINE - 已連線 - - - ERROR - 錯誤 - - - SUNNYLINK - - - - - SoftwarePanel - - Updates are only downloaded while the car is off. - 系統更新只會在熄火時下載。 - - - Current Version - 當前版本 - - - Download - 下載 - - - Install Update - 安裝更新 - - - INSTALL - 安裝 - - - Target Branch - 目標分支 - - - SELECT - 選取 - - - Select a branch - 選取一個分支 - - - UNINSTALL - 解除安裝 - - - Uninstall %1 - 解除安裝 %1 - - - Are you sure you want to uninstall? - 您確定您要解除安裝嗎? - - - CHECK - 檢查 - - - Uninstall - 解除安裝 - - - failed to check for update - 檢查更新失敗 - - - up to date, last checked %1 - 已經是最新版本,上次檢查時間為 %1 - - - DOWNLOAD - 下載 - - - update available - 有可用的更新 - - - never - 從未更新 - - - - SoftwarePanelSP - - Search Branch - - - - Enter search keywords, or leave blank to list all branches. - - - - Disable Updates - - - - When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> - - - - No branches found for keywords: %1 - - - - Select a branch - 選取一個分支 - - - %1 updates requires a reboot.<br>Reboot now? - - - - Reboot - 重新啟動 - - - When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> - - - - Please enable always offroad mode or turn off vehicle to adjust these toggles - - - - - SpeedLimitPolicy - - Back - 回上頁 - - - Speed Limit Source - - - - ⦿ Car Only: Use Speed Limit data only from Car - - - - ⦿ Map Only: Use Speed Limit data only from OpenStreetMaps - - - - ⦿ Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps - - - - ⦿ Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car - - - - ⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps - - - - - SpeedLimitSettings - - Back - 回上頁 - - - Speed Limit - - - - Customize Source - - - - Speed Limit Offset - - - - ⦿ None: No Offset - - - - ⦿ Fixed: Adds a fixed offset [Speed Limit + Offset] - - - - ⦿ Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)] - - - - ⦿ Off: Disables the Speed Limit functions. - - - - ⦿ Information: Displays the current road's speed limit. - - - - ⦿ Warning: Provides a warning when exceeding the current road's speed limit. - - - - ⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons. - - - - - SshControl - - SSH Keys - SSH 金鑰 - - - 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 使用者名稱。 - - - ADD - 新增 - - - Enter your GitHub username - 請輸入您 GitHub 的使用者名稱 - - - LOADING - 載入中 - - - REMOVE - 移除 - - - Username '%1' has no keys on GitHub - GitHub 用戶 '%1' 沒有設定任何金鑰 - - - Request timed out - 請求超時 - - - Username '%1' doesn't exist on GitHub - GitHub 用戶 '%1' 不存在 - - - - SshToggle - - Enable SSH - 啟用 SSH 服務 - - - - SunnylinkPanel - - This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - - - - Enable sunnylink - - - - Sponsor Status - - - - SPONSOR - - - - Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - - - - Pair GitHub Account - - - - PAIR - 配對 - - - Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - - - - N/A - 無法使用 - - - sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Backup Settings - - - - Are you sure you want to backup sunnypilot settings? - - - - Back Up - - - - Restore Settings - - - - Are you sure you want to restore the last backed up sunnypilot settings? - - - - Restore - - - - Backup in progress %1% - - - - Backup Failed - - - - Settings backup completed. - - - - Restore in progress %1% - - - - Restore Failed - - - - Unable to restore the settings, try again later. - - - - Settings restored. Confirm to restart the interface. - - - - Device ID - - - - THANKS ♥ - - - - Not Sponsor - - - - Paired - - - - Not Paired - - - - Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) - - - - [Don't use] Enable sunnylink uploader - - - - 🚀 sunnylink 🚀 - - - - For secure backup, restore, and remote configuration - - - - Sponsorship isn't required for basic backup/restore - - - - Click the sponsor button for more details - - - - - SunnylinkSponsorPopup - - Scan the QR code to login to your GitHub account - - - - Follow the prompts to complete the pairing process - - - - Re-enter the "sunnylink" panel to verify sponsorship status - - - - If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - - - - Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - - - - Choose your sponsorship tier and confirm your support - - - - Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - - - - Pair your GitHub account - - - - Early Access: Become a sunnypilot Sponsor - - - - - TermsPage - - Decline - 拒絕 - - - Agree - 接受 - - - Welcome to sunnypilot - - - - You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - - - - - TogglesPanel - - Enable Lane Departure Warnings - 啟用車道偏離警告 - - - Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). - 車速在時速 50 公里 (31 英里) 以上且未打方向燈的情況下,如果偵測到車輛駛出目前車道線時,發出車道偏離警告。 - - - Use Metric System - 使用公制單位 - - - Display speed in km/h instead of mph. - 啟用後,速度單位顯示將從 mp/h 改為 km/h。 - - - Record and Upload Driver Camera - 記錄並上傳駕駛監控影像 - - - Upload data from the driver facing camera and help improve the driver monitoring algorithm. - 上傳駕駛監控的錄影來協助我們提升駕駛監控的準確率。 - - - Disengage on Accelerator Pedal - 油門取消控車 - - - Experimental Mode - 實驗模式 - - - New Driving Visualization - 新的駕駛視覺介面 - - - Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - 因車輛使用內建ACC系統,無法在本車輛上啟動實驗模式。 - - - openpilot longitudinal control may come in a future update. - openpilot 縱向控制可能會在未來的更新中提供。 - - - Aggressive - 積極 - - - Standard - 標準 - - - Relaxed - 舒適 - - - Driving Personality - 駕駛風格 - - - End-to-End Longitudinal Control - 端到端縱向控制 - - - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - 在低速時,駕駛可視化將切換至道路朝向的廣角攝影機,以更好地顯示某些彎道。在右上角還會顯示「實驗模式」的標誌。 - - - Always-On Driver Monitoring - 駕駛監控常開 - - - Changing this setting will restart openpilot if the car is powered on. - 若車輛電源為開啟狀態,變更此設定將會重新啟動 openpilot。 - - - Record and Upload Microphone Audio - 錄製並上傳麥克風音訊 - - - Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - 在駕駛時錄製並儲存麥克風音訊。此音訊將會收錄在 comma connect 的行車記錄器影片中。 - - - Enable sunnypilot - - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - - - - Enable Dynamic Experimental Control - - - - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - - - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - Enable driver monitoring even when sunnypilot is not engaged. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - - TorqueLateralControlCustomParams - - Manual Real-Time Tuning - - - - Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values. - - - - Lateral Acceleration Factor - - - - Friction - - - - Real-time and Offline - - - - Offline Only - - - - - TorqueLateralControlSettings - - Self-Tune - - - - Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default. - - - - Less Restrict Settings for Self-Tune (Beta) - - - - Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values. - - - - Enable Custom Tuning - - - - Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also be used live when "Manual Real-Time Tuning" toggle is enabled. - - - - - TreeOptionDialog - - Select - 選擇 - - - Cancel - 取消 - - - Favorites - - - - - VisualsPanel - - Show Blind Spot Warnings - - - - Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. - - - - Changing this setting will restart openpilot if the car is powered on. - 若車輛電源為開啟狀態,變更此設定將會重新啟動 openpilot。 - - - Off - - - - Distance - - - - Speed - - - - Time - - - - All - - - - Display Metrics Below Chevron - - - - Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). - - - - Enable Tesla Rainbow Mode - - - - A beautiful rainbow effect on the path the model wants to take. - - - - It - - - - does not - - - - affect driving in any way. - - - - Enable Standstill Timer - - - - Show a timer on the HUD when the car is at a standstill. - - - - Display Road Name - - - - Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be downloaded from the OSM panel to fetch the road name. - - - - Green Traffic Light Alert (Beta) - - - - A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Lead Departure Alert (Beta) - - - - A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving. - - - - Speedometer: Always Display True Speed - - - - Always display the true vehicle current speed from wheel speed sensors. - - - - Speedometer: Hide from Onroad Screen - - - - Right - - - - Right && -Bottom - - - - Developer UI - - - - Display real-time parameters and metrics from various sources. - - - - - 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 模式 <span style='font-family: Noto Color Emoji;'>🔥</span> - - - - WifiUI - - Scanning for networks... - 掃描無線網路中... - - - CONNECTING... - 連線中... - - - FORGET - 清除 - - - Forget Wi-Fi Network "%1"? - 清除 Wi-Fi 網路 "%1"? - - - Forget - 清除 - - - diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc deleted file mode 100644 index 6bca2b7f7f..0000000000 --- a/selfdrive/ui/ui.cc +++ /dev/null @@ -1,245 +0,0 @@ -#include "selfdrive/ui/ui.h" - -#include -#include - -#include - -#include "common/transformations/orientation.hpp" -#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) { - s->sm->update(0); -} - -void update_state(UIState *s) { - SubMaster &sm = *(s->sm); - UIScene &scene = s->scene; - - if (sm.updated("liveCalibration")) { - auto list2rot = [](const capnp::List::Reader &rpy_list) ->Eigen::Matrix3f { - return euler2rot({rpy_list[0], rpy_list[1], rpy_list[2]}).cast(); - }; - - auto live_calib = sm["liveCalibration"].getLiveCalibration(); - if (live_calib.getCalStatus() == cereal::LiveCalibrationData::Status::CALIBRATED) { - auto device_from_calib = list2rot(live_calib.getRpyCalib()); - auto wide_from_device = list2rot(live_calib.getWideFromDeviceEuler()); - s->scene.view_from_calib = VIEW_FROM_DEVICE * device_from_calib; - s->scene.view_from_wide_calib = VIEW_FROM_DEVICE * wide_from_device * device_from_calib; - } else { - s->scene.view_from_calib = s->scene.view_from_wide_calib = VIEW_FROM_DEVICE; - } - } - if (sm.updated("pandaStates")) { - auto pandaStates = sm["pandaStates"].getPandaStates(); - if (pandaStates.size() > 0) { - scene.pandaType = pandaStates[0].getPandaType(); - - if (scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) { - scene.ignition = false; - for (const auto& pandaState : pandaStates) { - scene.ignition |= pandaState.getIgnitionLine() || pandaState.getIgnitionCan(); - } - } - } - } else if ((s->sm->frame - s->sm->rcv_frame("pandaStates")) > 5*UI_FREQ) { - scene.pandaType = cereal::PandaState::PandaType::UNKNOWN; - } - if (sm.updated("wideRoadCameraState")) { - auto cam_state = sm["wideRoadCameraState"].getWideRoadCameraState(); - scene.light_sensor = std::max(100.0f - cam_state.getExposureValPercent(), 0.0f); - } else if (!sm.allAliveAndValid({"wideRoadCameraState"})) { - scene.light_sensor = -1; - } - scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition; - - auto params = Params(); - scene.recording_audio = params.getBool("RecordAudio") && scene.started; -} - -void ui_update_params(UIState *s) { - auto params = Params(); - s->scene.is_metric = params.getBool("IsMetric"); -} - -void UIState::updateStatus() { - if (scene.started && (sm->updated("selfdriveState") || sm->updated("selfdriveStateSP"))) { - auto ss = (*sm)["selfdriveState"].getSelfdriveState(); - auto mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads(); - auto state = ss.getState(); - auto state_mads = mads.getState(); - if (state == cereal::SelfdriveState::OpenpilotState::PRE_ENABLED || state == cereal::SelfdriveState::OpenpilotState::OVERRIDING || - state_mads == cereal::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::PAUSED || - state_mads == cereal::ModularAssistiveDrivingSystem::ModularAssistiveDrivingSystemState::OVERRIDING) { - status = STATUS_OVERRIDE; - } else { - if (mads.getAvailable()) { - 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; - } - } else { - status = ss.getEnabled() ? STATUS_ENGAGED : STATUS_DISENGAGED; - } - } - } - - if (engaged() != engaged_prev) { - engaged_prev = engaged(); - emit engagedChanged(engaged()); - } - - // Handle onroad/offroad transition - if (scene.started != started_prev || sm->frame == 1) { - if (scene.started) { - status = STATUS_DISENGAGED; - scene.started_frame = sm->frame; - } - started_prev = scene.started; - emit offroadTransition(!scene.started); - } -} - -UIState::UIState(QObject *parent) : QObject(parent) { - sm = std::make_unique(std::vector{ - "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", - "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", - "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - }); - prime_state = new PrimeState(this); - language = QString::fromStdString(Params().get("LanguageSetting")); - -#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() { -#ifndef SUNNYPILOT - update_sockets(this); - update_state(this); - updateStatus(); - - if (sm->frame % UI_FREQ == 0) { - watchdog_kick(nanos_since_boot()); - } - emit uiUpdate(*this); -#endif -} - -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) { - updateBrightness(s); - updateWakefulness(s); -} - -void Device::setAwake(bool on) { - if (on != awake) { - awake = on; - Hardware::set_display_power(awake); - LOGD("setting display power %d", awake); - emit displayPowerChanged(awake); - } -} - -void Device::resetInteractiveTimeout(int timeout) { - int customTimeout = QString::fromStdString(Params().get("InteractivityTimeout")).toInt(); - if (timeout == -1) { - timeout = customTimeout == 0 ? (ignition_on ? 10 : 30) : customTimeout; - } - interactive_timeout = timeout * UI_FREQ; -} - -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; - - // CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm - if (clipped_brightness <= 8) { - clipped_brightness = (clipped_brightness / 903.3); - } else { - clipped_brightness = std::pow((clipped_brightness + 16.0) / 116.0, 3.0); - } - - 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; - } - - if (!awake) { - brightness = 0; - } - - // Onroad Brightness Control -#ifdef SUNNYPILOT - if (awake && s.scene.started && s.scene.onroadScreenOffControl && s.scene.onroadScreenOffTimer == 0) { - brightness = s.scene.onroadScreenOffBrightness * 0.01 * brightness; - } -#endif - - if (brightness != last_brightness) { - if (!brightness_future.isRunning()) { - brightness_future = QtConcurrent::run(Hardware::set_brightness, brightness); - last_brightness = brightness; - } - } -} - -void Device::updateWakefulness(const UIState &s) { - bool ignition_just_turned_off = !s.scene.ignition && ignition_on; - ignition_on = s.scene.ignition; - - if (ignition_just_turned_off) { - resetInteractiveTimeout(); - } else if (interactive_timeout > 0 && --interactive_timeout == 0) { - emit interactiveTimeout(); - } - - setAwake(s.scene.ignition || interactive_timeout > 0); -} - -#ifndef SUNNYPILOT -UIState *uiState() { - static UIState ui_state; - return &ui_state; -} - -Device *device() { - static Device _device; - return &_device; -} -#endif diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h deleted file mode 100644 index 5b3872b3d4..0000000000 --- a/selfdrive/ui/ui.h +++ /dev/null @@ -1,150 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include - -#include "cereal/messaging/messaging.h" -#include "common/mat.h" -#include "common/params.h" -#include "common/util.h" -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/prime_state.h" - -const int UI_BORDER_SIZE = 30; -const int UI_HEADER_HEIGHT = 420; - -const int UI_FREQ = 20; // Hz -const int BACKLIGHT_OFFROAD = 50; - -const Eigen::Matrix3f VIEW_FROM_DEVICE = (Eigen::Matrix3f() << - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0, - 1.0, 0.0, 0.0).finished(); - -const Eigen::Matrix3f FCAM_INTRINSIC_MATRIX = (Eigen::Matrix3f() << - 2648.0, 0.0, 1928.0 / 2, - 0.0, 2648.0, 1208.0 / 2, - 0.0, 0.0, 1.0).finished(); - -// tici ecam focal probably wrong? magnification is not consistent across frame -// Need to retrain model before this can be changed -const Eigen::Matrix3f ECAM_INTRINSIC_MATRIX = (Eigen::Matrix3f() << - 567.0, 0.0, 1928.0 / 2, - 0.0, 567.0, 1208.0 / 2, - 0.0, 0.0, 1.0).finished(); - -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 { - Eigen::Matrix3f view_from_calib = VIEW_FROM_DEVICE; - Eigen::Matrix3f view_from_wide_calib = VIEW_FROM_DEVICE; - cereal::PandaState::PandaType pandaType; - - cereal::LongitudinalPersonality personality; - - float light_sensor = -1; - bool started, ignition, is_metric, recording_audio; - uint64_t started_frame; -} UIScene; - -#ifdef SUNNYPILOT -#include "sunnypilot/ui_scene.h" -#define UIScene UISceneSP -#endif - -class UIState : public QObject { - Q_OBJECT - -public: - UIState(QObject* parent = 0); - virtual void updateStatus(); - virtual inline bool engaged() const { - return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled(); - } - - std::unique_ptr sm; - UIStatus status; - UIScene scene = {}; - QString language; - PrimeState *prime_state; - -signals: - void uiUpdate(const UIState &s); - void offroadTransition(bool offroad); - void engagedChanged(bool engaged); - -protected slots: - virtual void update(); - -protected: - QTimer *timer; - -private: - bool started_prev = false; - bool engaged_prev = false; -}; - -#ifndef SUNNYPILOT -UIState *uiState(); -#endif - -// device management class -class Device : public QObject { - Q_OBJECT - -public: - Device(QObject *parent = 0); - bool isAwake() { return awake; } - void setOffroadBrightness(int brightness) { - offroad_brightness = std::clamp(brightness, 0, 100); - } - -protected: - bool awake = false; - int interactive_timeout = 0; - bool ignition_on = false; - - int offroad_brightness = BACKLIGHT_OFFROAD; - int last_brightness = 0; - FirstOrderFilter brightness_filter; - QFuture brightness_future; - - void updateBrightness(const UIState &s); - void updateWakefulness(const UIState &s); - void setAwake(bool on); - -signals: - void displayPowerChanged(bool on); - void interactiveTimeout(); - -public slots: - void resetInteractiveTimeout(int timeout = -1); - 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/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index bb2b431e03..a4b99825e9 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -1,23 +1,32 @@ #!/usr/bin/env python3 +import os import pyray as rl -from openpilot.common.watchdog import kick_watchdog + +from openpilot.system.hardware import TICI +from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.main import MainLayout from openpilot.selfdrive.ui.ui_state import ui_state def main(): + cores = {5, } + config_realtime_process(0, 51) + gui_app.init_window("UI") main_layout = MainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - for _ in gui_app.render(): + for should_render in gui_app.render(): ui_state.update() + if should_render: + main_layout.render() - # TODO handle brigntness and awake state here - - main_layout.render() - - kick_watchdog() + # reaffine after power save offlines our core + if TICI and os.sched_getaffinity(0) != cores: + try: + set_core_affinity(list(cores)) + except OSError: + pass if __name__ == "__main__": diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index c4a2c0ca11..b08b8ef28c 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -4,16 +4,14 @@ import time import threading from collections.abc import Callable from enum import Enum -from cereal import messaging, log +from cereal import messaging, car, log from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.params import Params, UnknownKeyName +from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState -from openpilot.system.ui.lib.application import DEFAULT_FPS -from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app +from openpilot.system.hardware import HARDWARE -UI_BORDER_SIZE = 30 BACKLIGHT_OFFROAD = 50 @@ -51,6 +49,7 @@ class UIState: "managerState", "selfdriveState", "longitudinalPlan", + "rawAudioData", ] ) @@ -59,6 +58,7 @@ class UIState: # UI Status tracking self.status: UIStatus = UIStatus.DISENGAGED self.started_frame: int = 0 + self.started_time: float = 0.0 self._engaged_prev: bool = False self._started_prev: bool = False @@ -66,11 +66,25 @@ class UIState: self.is_metric: bool = self.params.get_bool("IsMetric") self.started: bool = False self.ignition: bool = False + self.recording_audio: bool = False self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard + self.has_longitudinal_control: bool = False + self.CP: car.CarParams | None = None self.light_sensor: float = -1.0 + self._param_update_time: float = 0.0 - self._update_params() + # Callbacks + self._offroad_transition_callbacks: list[Callable[[], None]] = [] + self._engaged_transition_callbacks: list[Callable[[], None]] = [] + + self.update_params() + + def add_offroad_transition_callback(self, callback: Callable[[], None]): + self._offroad_transition_callbacks.append(callback) + + def add_engaged_transition_callback(self, callback: Callable[[], None]): + self._engaged_transition_callbacks.append(callback) @property def engaged(self) -> bool: @@ -83,9 +97,12 @@ class UIState: return not self.started def update(self) -> None: + self.prime_state.start() # start thread after manager forks ui self.sm.update(0) self._update_state() self._update_status() + if time.monotonic() - self._param_update_time > 5.0: + self.update_params() device.update() def _update_state(self) -> None: @@ -112,6 +129,11 @@ class UIState: # Update started state self.started = self.sm["deviceState"].started and self.ignition + # Update recording audio state + self.recording_audio = self.params.get_bool("RecordAudio") and self.started + + self.is_metric = self.params.get_bool("IsMetric") + def _update_status(self) -> None: if self.started and self.sm.updated["selfdriveState"]: ss = self.sm["selfdriveState"] @@ -124,6 +146,8 @@ class UIState: # Check for engagement state changes if self.engaged != self._engaged_prev: + for callback in self._engaged_transition_callbacks: + callback() self._engaged_prev = self.engaged # Handle onroad/offroad transition @@ -131,14 +155,24 @@ class UIState: if self.started: self.status = UIStatus.DISENGAGED self.started_frame = self.sm.frame + self.started_time = time.monotonic() + + for callback in self._offroad_transition_callbacks: + callback() self._started_prev = self.started - def _update_params(self) -> None: - try: - self.is_metric = self.params.get_bool("IsMetric") - except UnknownKeyName: - self.is_metric = False + def update_params(self) -> None: + # For slower operations + # Update longitudinal control state + CP_bytes = self.params.get("CarParamsPersistent") + if CP_bytes is not None: + self.CP = messaging.log_from_bytes(CP_bytes, car.CarParams) + if self.CP.alphaLongitudinalAvailable: + self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled") + else: + self.has_longitudinal_control = self.CP.openpilotLongitudinalControl + self._param_update_time = time.monotonic() class Device: @@ -151,7 +185,7 @@ class Device: self._offroad_brightness: int = BACKLIGHT_OFFROAD self._last_brightness: int = 0 - self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / DEFAULT_FPS) + self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / gui_app.target_fps) self._brightness_thread: threading.Thread | None = None def reset_interactive_timeout(self, timeout: int = -1) -> None: @@ -194,7 +228,6 @@ class Device: if brightness != self._last_brightness: if self._brightness_thread is None or not self._brightness_thread.is_alive(): - cloudlog.debug(f"setting display brightness {brightness}") self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,)) self._brightness_thread.start() self._last_brightness = brightness @@ -220,6 +253,7 @@ class Device: self._awake = on cloudlog.debug(f"setting display power {int(on)}") HARDWARE.set_display_power(on) + gui_app.set_should_render(on) # Global instance diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index 65880bdad9..bded80b2e5 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -1,50 +1,42 @@ #!/usr/bin/env python3 -import argparse -import json +from itertools import chain import os - from openpilot.common.basedir import BASEDIR +from openpilot.system.ui.lib.multilang import SYSTEM_UI_DIR, UI_DIR, TRANSLATIONS_DIR, multilang -UI_DIR = os.path.join(BASEDIR, "selfdrive", "ui") -TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations") -LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") -TRANSLATIONS_INCLUDE_FILE = os.path.join(TRANSLATIONS_DIR, "alerts_generated.h") -PLURAL_ONLY = ["main_en"] # base language, only create entries for strings with plural forms +LANGUAGES_FILE = os.path.join(str(TRANSLATIONS_DIR), "languages.json") +POT_FILE = os.path.join(str(TRANSLATIONS_DIR), "app.pot") -def generate_translations_include(): - # offroad alerts - # TODO translate events from openpilot.selfdrive/controls/lib/events.py - content = "// THIS IS AN AUTOGENERATED FILE, PLEASE EDIT alerts_offroad.json\n" - with open(os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json")) as f: - for alert in json.load(f).values(): - content += f'QT_TRANSLATE_NOOP("OffroadAlert", R"({alert["text"]})");\n' +def update_translations(): + files = [] + for root, _, filenames in chain(os.walk(SYSTEM_UI_DIR), + os.walk(os.path.join(UI_DIR, "widgets")), + os.walk(os.path.join(UI_DIR, "layouts")), + os.walk(os.path.join(UI_DIR, "onroad"))): + for filename in filenames: + if filename.endswith(".py"): + files.append(os.path.relpath(os.path.join(root, filename), BASEDIR)) - with open(TRANSLATIONS_INCLUDE_FILE, "w") as f: - f.write(content) + # Create main translation file + cmd = ("xgettext -L Python --keyword=tr --keyword=trn:1,2 --keyword=tr_noop --from-code=UTF-8 " + + "--flag=tr:1:python-brace-format --flag=trn:1:python-brace-format --flag=trn:2:python-brace-format " + + f"-D {BASEDIR} -o {POT_FILE} {' '.join(files)}") + ret = os.system(cmd) + assert ret == 0 -def update_translations(vanish: bool = False, translation_files: None | list[str] = None, translations_dir: str = TRANSLATIONS_DIR): - if translation_files is None: - with open(LANGUAGES_FILE) as f: - translation_files = json.load(f).values() - - for file in translation_files: - tr_file = os.path.join(translations_dir, f"{file}.ts") - args = f"lupdate -locations none -recursive {UI_DIR} -ts {tr_file} -I {BASEDIR}" - if vanish: - args += " -no-obsolete" - if file in PLURAL_ONLY: - args += " -pluralonly" - ret = os.system(args) - assert ret == 0 + # Generate/update translation files for each language + for name in multilang.languages.values(): + if os.path.exists(os.path.join(TRANSLATIONS_DIR, f"app_{name}.po")): + cmd = f"msgmerge --update --no-fuzzy-matching --backup=none --sort-output {TRANSLATIONS_DIR}/app_{name}.po {POT_FILE}" + ret = os.system(cmd) + assert ret == 0 + else: + cmd = f"msginit -l {name} --no-translator --input {POT_FILE} --output-file {TRANSLATIONS_DIR}/app_{name}.po" + ret = os.system(cmd) + assert ret == 0 if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Update translation files for UI", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--vanish", action="store_true", help="Remove translations with source text no longer found") - args = parser.parse_args() - - generate_translations_include() - update_translations(args.vanish) + update_translations() diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py index 9618768957..faa3bf877f 100644 --- a/selfdrive/ui/widgets/exp_mode_button.py +++ b/selfdrive/ui/widgets/exp_mode_button.py @@ -1,6 +1,7 @@ import pyray as rl from openpilot.common.params import Params -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget @@ -9,7 +10,7 @@ class ExperimentalModeButton(Widget): super().__init__() self.img_width = 80 - self.horizontal_padding = 50 + self.horizontal_padding = 25 self.button_height = 125 self.params = Params() @@ -18,6 +19,9 @@ class ExperimentalModeButton(Widget): self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width) self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width) + def show_event(self): + self.experimental_mode = self.params.get_bool("ExperimentalMode") + def _get_gradient_colors(self): alpha = 0xCC if self.is_pressed else 0xFF @@ -31,16 +35,10 @@ class ExperimentalModeButton(Widget): rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height), start_color, end_color) - def _handle_mouse_release(self, mouse_pos): - self.experimental_mode = not self.experimental_mode - # TODO: Opening settings for ExperimentalMode - self.params.put_bool("ExperimentalMode", self.experimental_mode) - def _render(self, rect): - rl.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE) - rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) self._draw_gradient_background(rect) + rl.draw_rectangle_rounded_lines_ex(self._rect, 0.19, 10, 5, rl.BLACK) rl.end_scissor_mode() # Draw vertical separator line @@ -49,9 +47,9 @@ class ExperimentalModeButton(Widget): rl.draw_line_ex(rl.Vector2(line_x, rect.y), rl.Vector2(line_x, rect.y + rect.height), 3, separator_color) # Draw text label (left aligned) - text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON" + text = tr("EXPERIMENTAL MODE ON") if self.experimental_mode else tr("CHILL MODE ON") text_x = rect.x + self.horizontal_padding - text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically + text_y = rect.y + rect.height / 2 - 45 * FONT_SCALE // 2 # Center vertically rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK) diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index f54bd9116c..802243ff3e 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -1,15 +1,18 @@ -import json import pyray as rl +from enum import IntEnum from abc import ABC, abstractmethod from collections.abc import Callable from dataclasses import dataclass from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.html_render import HtmlRenderer +from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS class AlertColors: @@ -17,21 +20,23 @@ class AlertColors: LOW_SEVERITY = rl.Color(41, 41, 41, 255) BACKGROUND = rl.Color(57, 57, 57, 255) BUTTON = rl.WHITE + BUTTON_PRESSED = rl.Color(200, 200, 200, 255) BUTTON_TEXT = rl.BLACK SNOOZE_BG = rl.Color(79, 79, 79, 255) + SNOOZE_BG_PRESSED = rl.Color(100, 100, 100, 255) TEXT = rl.WHITE class AlertConstants: - BUTTON_SIZE = (400, 125) - SNOOZE_BUTTON_SIZE = (550, 125) - REBOOT_BUTTON_SIZE = (600, 125) + MIN_BUTTON_WIDTH = 400 + BUTTON_HEIGHT = 125 MARGIN = 50 SPACING = 30 FONT_SIZE = 48 - BORDER_RADIUS = 30 + BORDER_RADIUS = 30 * 2 # matches Qt's 30px ALERT_HEIGHT = 120 - ALERT_SPACING = 20 + ALERT_SPACING = 10 + ALERT_INSET = 60 @dataclass @@ -42,6 +47,43 @@ class AlertData: visible: bool = False +class ButtonStyle(IntEnum): + LIGHT = 0 + DARK = 1 + + +class ActionButton(Widget): + def __init__(self, text: str | Callable[[], str], style: ButtonStyle = ButtonStyle.LIGHT, + min_width: int = AlertConstants.MIN_BUTTON_WIDTH): + super().__init__() + self._text = text + self._style = style + self._min_width = min_width + self._font = gui_app.font(FontWeight.MEDIUM) + + @property + def text(self) -> str: + return self._text() if callable(self._text) else self._text + + def _render(self, _): + text_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), self.text, AlertConstants.FONT_SIZE) + self._rect.width = max(text_size.x + 60 * 2, self._min_width) + self._rect.height = AlertConstants.BUTTON_HEIGHT + + roundness = AlertConstants.BORDER_RADIUS / self._rect.height + bg_color = AlertColors.BUTTON if self._style == ButtonStyle.LIGHT else AlertColors.SNOOZE_BG + if self.is_pressed: + bg_color = AlertColors.BUTTON_PRESSED if self._style == ButtonStyle.LIGHT else AlertColors.SNOOZE_BG_PRESSED + + rl.draw_rectangle_rounded(self._rect, roundness, 10, bg_color) + + # center text + color = rl.WHITE if self._style == ButtonStyle.DARK else rl.BLACK + text_x = int(self._rect.x + (self._rect.width - text_size.x) // 2) + text_y = int(self._rect.y + (self._rect.height - text_size.y) // 2) + rl.draw_text_ex(self._font, self.text, rl.Vector2(text_x, text_y), AlertConstants.FONT_SIZE, 0, color) + + class AbstractAlert(Widget, ABC): def __init__(self, has_reboot_btn: bool = False): super().__init__() @@ -49,17 +91,38 @@ class AbstractAlert(Widget, ABC): self.has_reboot_btn = has_reboot_btn self.dismiss_callback: Callable | None = None - self.dismiss_btn_rect = rl.Rectangle(0, 0, *AlertConstants.BUTTON_SIZE) - self.snooze_btn_rect = rl.Rectangle(0, 0, *AlertConstants.SNOOZE_BUTTON_SIZE) - self.reboot_btn_rect = rl.Rectangle(0, 0, *AlertConstants.REBOOT_BUTTON_SIZE) + def snooze_callback(): + self.params.put_bool("SnoozeUpdate", True) + if self.dismiss_callback: + self.dismiss_callback() - self.snooze_visible = False + def excessive_actuation_callback(): + self.params.remove("Offroad_ExcessiveActuation") + if self.dismiss_callback: + self.dismiss_callback() + + self.dismiss_btn = ActionButton(lambda: tr("Close")) + + self.snooze_btn = ActionButton(lambda: tr("Snooze Update"), style=ButtonStyle.DARK) + self.snooze_btn.set_click_callback(snooze_callback) + + self.excessive_actuation_btn = ActionButton(lambda: tr("Acknowledge Excessive Actuation"), style=ButtonStyle.DARK, min_width=800) + self.excessive_actuation_btn.set_click_callback(excessive_actuation_callback) + + self.reboot_btn = ActionButton(lambda: tr("Reboot and Update"), min_width=600) + self.reboot_btn.set_click_callback(lambda: HARDWARE.reboot()) + + # TODO: just use a Scroller? self.content_rect = rl.Rectangle(0, 0, 0, 0) self.scroll_panel_rect = rl.Rectangle(0, 0, 0, 0) self.scroll_panel = GuiScrollPanel() + def show_event(self): + self.scroll_panel.set_offset(0) + def set_dismiss_callback(self, callback: Callable): self.dismiss_callback = callback + self.dismiss_btn.set_click_callback(self.dismiss_callback) @abstractmethod def refresh(self) -> bool: @@ -69,31 +132,10 @@ class AbstractAlert(Widget, ABC): def get_content_height(self) -> float: pass - def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool: - if not mouse_clicked or not self.scroll_panel.is_touch_valid(): - return False - - if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect): - if self.dismiss_callback: - self.dismiss_callback() - return True - - if self.snooze_visible and rl.check_collision_point_rec(mouse_pos, self.snooze_btn_rect): - self.params.put_bool("SnoozeUpdate", True) - if self.dismiss_callback: - self.dismiss_callback() - return True - - if self.has_reboot_btn and rl.check_collision_point_rec(mouse_pos, self.reboot_btn_rect): - HARDWARE.reboot() - return True - - return False - def _render(self, rect: rl.Rectangle): - rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND) + rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.height, 10, AlertColors.BACKGROUND) - footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING + footer_height = AlertConstants.BUTTON_HEIGHT + AlertConstants.SPACING content_height = rect.height - 2 * AlertConstants.MARGIN - footer_height self.content_rect = rl.Rectangle( @@ -112,7 +154,7 @@ class AbstractAlert(Widget, ABC): def _render_scrollable_content(self): content_total_height = self.get_content_height() content_bounds = rl.Rectangle(0, 0, self.scroll_panel_rect.width, content_total_height) - scroll_offset = self.scroll_panel.handle_scroll(self.scroll_panel_rect, content_bounds) + scroll_offset = self.scroll_panel.update(self.scroll_panel_rect, content_bounds) rl.begin_scissor_mode( int(self.scroll_panel_rect.x), @@ -123,7 +165,7 @@ class AbstractAlert(Widget, ABC): content_rect_with_scroll = rl.Rectangle( self.scroll_panel_rect.x, - self.scroll_panel_rect.y + scroll_offset.y, + self.scroll_panel_rect.y + scroll_offset, self.scroll_panel_rect.width, content_total_height, ) @@ -136,44 +178,26 @@ class AbstractAlert(Widget, ABC): pass def _render_footer(self, rect: rl.Rectangle): - footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_SIZE[1] - font = gui_app.font(FontWeight.MEDIUM) + footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_HEIGHT - self.dismiss_btn_rect.x = rect.x + AlertConstants.MARGIN - self.dismiss_btn_rect.y = footer_y - rl.draw_rectangle_rounded(self.dismiss_btn_rect, 0.3, 10, AlertColors.BUTTON) + dismiss_x = rect.x + AlertConstants.MARGIN + self.dismiss_btn.set_position(dismiss_x, footer_y) + self.dismiss_btn.render() - text = "Close" - text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x - text_x = self.dismiss_btn_rect.x + (AlertConstants.BUTTON_SIZE[0] - text_width) // 2 - text_y = self.dismiss_btn_rect.y + (AlertConstants.BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 - rl.draw_text_ex( - font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT - ) + if self.has_reboot_btn: + reboot_x = rect.x + rect.width - AlertConstants.MARGIN - self.reboot_btn.rect.width + self.reboot_btn.set_position(reboot_x, footer_y) + self.reboot_btn.render() - if self.snooze_visible: - self.snooze_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.SNOOZE_BUTTON_SIZE[0] - self.snooze_btn_rect.y = footer_y - rl.draw_rectangle_rounded(self.snooze_btn_rect, 0.3, 10, AlertColors.SNOOZE_BG) + elif self.excessive_actuation_btn.is_visible: + actuation_x = rect.x + rect.width - AlertConstants.MARGIN - self.excessive_actuation_btn.rect.width + self.excessive_actuation_btn.set_position(actuation_x, footer_y) + self.excessive_actuation_btn.render() - text = "Snooze Update" - text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x - text_x = self.snooze_btn_rect.x + (AlertConstants.SNOOZE_BUTTON_SIZE[0] - text_width) // 2 - text_y = self.snooze_btn_rect.y + (AlertConstants.SNOOZE_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 - rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.TEXT) - - elif self.has_reboot_btn: - self.reboot_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.REBOOT_BUTTON_SIZE[0] - self.reboot_btn_rect.y = footer_y - rl.draw_rectangle_rounded(self.reboot_btn_rect, 0.3, 10, AlertColors.BUTTON) - - text = "Reboot and Update" - text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x - text_x = self.reboot_btn_rect.x + (AlertConstants.REBOOT_BUTTON_SIZE[0] - text_width) // 2 - text_y = self.reboot_btn_rect.y + (AlertConstants.REBOOT_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 - rl.draw_text_ex( - font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT - ) + elif self.snooze_btn.is_visible: + snooze_x = rect.x + rect.width - AlertConstants.MARGIN - self.snooze_btn.rect.width + self.snooze_btn.set_position(snooze_x, footer_y) + self.snooze_btn.render() class OffroadAlert(AbstractAlert): @@ -187,13 +211,14 @@ class OffroadAlert(AbstractAlert): active_count = 0 connectivity_needed = False + excessive_actuation = False for alert_data in self.sorted_alerts: text = "" alert_json = self.params.get(alert_data.key) if alert_json: - text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) + text = alert_json.get("text", "").replace("%1", alert_json.get("extra", "")) alert_data.text = text alert_data.visible = bool(text) @@ -204,7 +229,11 @@ class OffroadAlert(AbstractAlert): if alert_data.key == "Offroad_ConnectivityNeeded" and alert_data.visible: connectivity_needed = True - self.snooze_visible = connectivity_needed + if alert_data.key == "Offroad_ExcessiveActuation" and alert_data.visible: + excessive_actuation = True + + self.excessive_actuation_btn.set_visible(excessive_actuation) + self.snooze_btn.set_visible(connectivity_needed and not excessive_actuation) return active_count def get_content_height(self) -> float: @@ -218,12 +247,12 @@ class OffroadAlert(AbstractAlert): if not alert_data.visible: continue - text_width = int(self.content_rect.width - 90) + text_width = int(self.content_rect.width - (AlertConstants.ALERT_INSET * 2)) wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width) line_count = len(wrapped_lines) - text_height = line_count * (AlertConstants.FONT_SIZE + 5) - alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT) - total_height += alert_item_height + AlertConstants.ALERT_SPACING + text_height = line_count * (AlertConstants.FONT_SIZE * FONT_SCALE) + alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT) + total_height += round(alert_item_height + AlertConstants.ALERT_SPACING) if total_height > 20: total_height = total_height - AlertConstants.ALERT_SPACING + 20 @@ -232,18 +261,13 @@ class OffroadAlert(AbstractAlert): def _build_alerts(self): self.sorted_alerts = [] - try: - with open("../selfdrived/alerts_offroad.json", "rb") as f: - alerts_config = json.load(f) - for key, config in sorted(alerts_config.items(), key=lambda x: x[1].get("severity", 0), reverse=True): - severity = config.get("severity", 0) - alert_data = AlertData(key=key, text="", severity=severity) - self.sorted_alerts.append(alert_data) - except (FileNotFoundError, json.JSONDecodeError): - pass + for key, config in sorted(OFFROAD_ALERTS.items(), key=lambda x: x[1].get("severity", 0), reverse=True): + severity = config.get("severity", 0) + alert_data = AlertData(key=key, text="", severity=severity) + self.sorted_alerts.append(alert_data) def _render_content(self, content_rect: rl.Rectangle): - y_offset = 20 + y_offset = AlertConstants.ALERT_SPACING font = gui_app.font(FontWeight.NORMAL) for alert_data in self.sorted_alerts: @@ -251,11 +275,11 @@ class OffroadAlert(AbstractAlert): continue bg_color = AlertColors.HIGH_SEVERITY if alert_data.severity > 0 else AlertColors.LOW_SEVERITY - text_width = int(content_rect.width - 90) + text_width = int(content_rect.width - (AlertConstants.ALERT_INSET * 2)) wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width) line_count = len(wrapped_lines) - text_height = line_count * (AlertConstants.FONT_SIZE + 5) - alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT) + text_height = line_count * (AlertConstants.FONT_SIZE * FONT_SCALE) + alert_item_height = max(text_height + (AlertConstants.ALERT_INSET * 2), AlertConstants.ALERT_HEIGHT) alert_rect = rl.Rectangle( content_rect.x + 10, @@ -264,22 +288,23 @@ class OffroadAlert(AbstractAlert): alert_item_height, ) - rl.draw_rectangle_rounded(alert_rect, 0.2, 10, bg_color) + roundness = AlertConstants.BORDER_RADIUS / min(alert_rect.height, alert_rect.width) + rl.draw_rectangle_rounded(alert_rect, roundness, 10, bg_color) - text_x = alert_rect.x + 30 - text_y = alert_rect.y + 20 + text_x = alert_rect.x + AlertConstants.ALERT_INSET + text_y = alert_rect.y + AlertConstants.ALERT_INSET for i, line in enumerate(wrapped_lines): rl.draw_text_ex( font, line, - rl.Vector2(text_x, text_y + i * (AlertConstants.FONT_SIZE + 5)), + rl.Vector2(text_x, text_y + i * AlertConstants.FONT_SIZE * FONT_SCALE), AlertConstants.FONT_SIZE, 0, AlertColors.TEXT, ) - y_offset += alert_item_height + AlertConstants.ALERT_SPACING + y_offset += round(alert_item_height + AlertConstants.ALERT_SPACING) class UpdateAlert(AbstractAlert): @@ -288,12 +313,18 @@ class UpdateAlert(AbstractAlert): self.release_notes = "" self._wrapped_release_notes = "" self._cached_content_height: float = 0.0 + self._html_renderer = HtmlRenderer(text="") def refresh(self) -> bool: update_available: bool = self.params.get_bool("UpdateAvailable") + no_release_notes = "

" + tr("No release notes available.") + "

" + if update_available: - self.release_notes = self.params.get("UpdaterNewReleaseNotes") + self.release_notes = (self.params.get("UpdaterNewReleaseNotes") or b"").decode("utf8").strip() + self._html_renderer.parse_html_content(self.release_notes or no_release_notes) self._cached_content_height = 0 + else: + self._html_renderer.parse_html_content(no_release_notes) return update_available @@ -309,18 +340,5 @@ class UpdateAlert(AbstractAlert): return self._cached_content_height def _render_content(self, content_rect: rl.Rectangle): - if self.release_notes: - rl.draw_text_ex( - gui_app.font(FontWeight.NORMAL), - self._wrapped_release_notes, - rl.Vector2(content_rect.x + 30, content_rect.y + 30), - AlertConstants.FONT_SIZE, - 0.0, - AlertColors.TEXT, - ) - else: - no_notes_text = "No release notes available." - text_width = rl.measure_text(no_notes_text, AlertConstants.FONT_SIZE) - text_x = content_rect.x + (content_rect.width - text_width) // 2 - text_y = content_rect.y + 50 - rl.draw_text(no_notes_text, int(text_x), int(text_y), AlertConstants.FONT_SIZE, AlertColors.TEXT) + notes_rect = rl.Rectangle(content_rect.x + 30, content_rect.y + 30, content_rect.width - 60, content_rect.height - 60) + self._html_renderer.render(notes_rect) diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py index 63298914ea..f960cf723e 100644 --- a/selfdrive/ui/widgets/pairing_dialog.py +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -6,29 +6,36 @@ import time from openpilot.common.api import Api from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets.button import IconButton +from openpilot.selfdrive.ui.ui_state import ui_state -class PairingDialog: +class PairingDialog(Widget): """Dialog for device pairing with QR code.""" QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds def __init__(self): + super().__init__() self.params = Params() self.qr_texture: rl.Texture | None = None - self.last_qr_generation = 0 + self.last_qr_generation = float('-inf') + self._close_btn = IconButton(gui_app.texture("icons/close.png", 80, 80)) + self._close_btn.set_click_callback(lambda: gui_app.set_modal_overlay(None)) def _get_pairing_url(self) -> str: try: dongle_id = self.params.get("DongleId") or "" - token = Api(dongle_id).get_token() - except Exception as e: - cloudlog.warning(f"Failed to get pairing token: {e}") + token = Api(dongle_id).get_token({'pair': True}) + except Exception: + cloudlog.exception("Failed to get pairing token") token = "" - return f"https://connect.comma.ai/setup?token={token}" + return f"https://connect.comma.ai/?pair={token}" def _generate_qr_code(self) -> None: try: @@ -50,8 +57,8 @@ class PairingDialog: rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 self.qr_texture = rl.load_texture_from_image(rl_image) - except Exception as e: - cloudlog.warning(f"QR code generation failed: {e}") + except Exception: + cloudlog.exception("QR code generation failed") self.qr_texture = None def _check_qr_refresh(self) -> None: @@ -60,7 +67,11 @@ class PairingDialog: self._generate_qr_code() self.last_qr_generation = current_time - def render(self, rect: rl.Rectangle) -> int: + def _update_state(self): + if ui_state.prime_state.is_paired(): + gui_app.set_modal_overlay(None) + + def _render(self, rect: rl.Rectangle) -> int: rl.clear_background(rl.Color(224, 224, 224, 255)) self._check_qr_refresh() @@ -71,24 +82,14 @@ class PairingDialog: # Close button close_size = 80 - close_icon = gui_app.texture("icons/close.png", close_size, close_size) - close_rect = rl.Rectangle(content_rect.x, y, close_size, close_size) - - mouse_pos = rl.get_mouse_position() - is_hover = rl.check_collision_point_rec(mouse_pos, close_rect) - is_pressed = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) - is_released = rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) - - color = rl.Color(180, 180, 180, 150) if (is_hover and is_pressed) else rl.WHITE - rl.draw_texture(close_icon, int(content_rect.x), int(y), color) - - if (is_hover and is_released) or rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): - return 1 + pad = 20 + close_rect = rl.Rectangle(content_rect.x - pad, y - pad, close_size + pad * 2, close_size + pad * 2) + self._close_btn.render(close_rect) y += close_size + 40 # Title - title = "Pair your device to your comma account" + title = tr("Pair your device to your comma account") title_font = gui_app.font(FontWeight.NORMAL) left_width = int(content_rect.width * 0.5 - 15) @@ -113,9 +114,9 @@ class PairingDialog: def _render_instructions(self, rect: rl.Rectangle) -> None: instructions = [ - "Go to https://connect.comma.ai on your phone", - "Click \"add new device\" and scan the QR code on the right", - "Bookmark connect.comma.ai to your home screen to use it like an app", + tr("Go to https://connect.comma.ai on your phone"), + tr("Click \"add new device\" and scan the QR code on the right"), + tr("Bookmark connect.comma.ai to your home screen to use it like an app"), ] font = gui_app.font(FontWeight.BOLD) @@ -134,8 +135,8 @@ class PairingDialog: # Circle and number rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255)) number = str(i + 1) - number_width = measure_text_cached(font, number, 30).x - rl.draw_text(number, int(circle_x - number_width // 2), int(circle_y - 15), 30, rl.WHITE) + number_size = measure_text_cached(font, number, 30) + rl.draw_text_ex(font, number, (int(circle_x - number_size.x // 2), int(circle_y - number_size.y // 2)), 30, 0, rl.WHITE) # Text rl.draw_text_ex(font, "\n".join(wrapped), rl.Vector2(text_x, y), 47, 0.0, rl.BLACK) @@ -146,7 +147,7 @@ class PairingDialog: rl.draw_rectangle_rounded(rect, 0.1, 20, rl.Color(240, 240, 240, 255)) error_font = gui_app.font(FontWeight.BOLD) rl.draw_text_ex( - error_font, "QR Code Error", rl.Vector2(rect.x + 20, rect.y + rect.height // 2 - 15), 30, 0.0, rl.RED + error_font, tr("QR Code Error"), rl.Vector2(rect.x + 20, rect.y + rect.height // 2 - 15), 30, 0.0, rl.RED ) return diff --git a/selfdrive/ui/widgets/prime.py b/selfdrive/ui/widgets/prime.py index 6b601f6dff..e98e4c1e1c 100644 --- a/selfdrive/ui/widgets/prime.py +++ b/selfdrive/ui/widgets/prime.py @@ -2,6 +2,7 @@ import pyray as rl from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.widgets import Widget @@ -22,41 +23,41 @@ class PrimeWidget(Widget): def _render_for_non_prime_users(self, rect: rl.Rectangle): """Renders the advertisement for non-Prime users.""" - rl.draw_rectangle_rounded(rect, 0.02, 10, self.PRIME_BG_COLOR) + rl.draw_rectangle_rounded(rect, 0.025, 10, self.PRIME_BG_COLOR) # Layout x, y = rect.x + 80, rect.y + 90 w = rect.width - 160 # Title - gui_label(rl.Rectangle(x, y, w, 90), "Upgrade Now", 75, font_weight=FontWeight.BOLD) + gui_label(rl.Rectangle(x, y, w, 90), tr("Upgrade Now"), 75, font_weight=FontWeight.BOLD) # Description with wrapping desc_y = y + 140 - font = gui_app.font(FontWeight.LIGHT) - wrapped_text = "\n".join(wrap_text(font, "Become a comma prime member at connect.comma.ai", 56, int(w))) + font = gui_app.font(FontWeight.NORMAL) + wrapped_text = "\n".join(wrap_text(font, tr("Become a comma prime member at connect.comma.ai"), 56, int(w))) text_size = measure_text_cached(font, wrapped_text, 56) rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.WHITE) # Features section features_y = desc_y + text_size.y + 50 - gui_label(rl.Rectangle(x, features_y, w, 50), "PRIME FEATURES:", 41, font_weight=FontWeight.BOLD) + gui_label(rl.Rectangle(x, features_y, w, 50), tr("PRIME FEATURES:"), 41, font_weight=FontWeight.BOLD) # Feature list - features = ["Remote access", "24/7 LTE connectivity", "1 year of drive storage", "Remote snapshots"] + features = [tr("Remote access"), tr("24/7 LTE connectivity"), tr("1 year of drive storage"), tr("Remote snapshots")] for i, feature in enumerate(features): item_y = features_y + 80 + i * 65 - gui_label(rl.Rectangle(x, item_y, 50, 60), "✓", 50, color=rl.Color(70, 91, 234, 255)) + gui_label(rl.Rectangle(x, item_y, 100, 60), "✓", 50, color=rl.Color(70, 91, 234, 255)) gui_label(rl.Rectangle(x + 60, item_y, w - 60, 60), feature, 50) def _render_for_prime_user(self, rect: rl.Rectangle): """Renders the prime user widget with subscription status.""" - rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.02, 10, self.PRIME_BG_COLOR) + rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.1, 10, self.PRIME_BG_COLOR) x = rect.x + 56 y = rect.y + 40 font = gui_app.font(FontWeight.BOLD) - rl.draw_text_ex(font, "✓ SUBSCRIBED", rl.Vector2(x, y), 41, 0, rl.Color(134, 255, 78, 255)) - rl.draw_text_ex(font, "comma prime", rl.Vector2(x, y + 61), 75, 0, rl.WHITE) + rl.draw_text_ex(font, tr("✓ SUBSCRIBED"), rl.Vector2(x, y), 41, 0, rl.Color(134, 255, 78, 255)) + rl.draw_text_ex(font, tr("comma prime"), rl.Vector2(x, y + 61), 75, 0, rl.WHITE) diff --git a/selfdrive/ui/widgets/setup.py b/selfdrive/ui/widgets/setup.py index 35a7c4101c..3c9406688f 100644 --- a/selfdrive/ui/widgets/setup.py +++ b/selfdrive/ui/widgets/setup.py @@ -1,11 +1,14 @@ import pyray as rl -from openpilot.selfdrive.ui.lib.prime_state import PrimeType +from openpilot.common.time_helpers import system_time_valid from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.confirm_dialog import alert_dialog +from openpilot.system.ui.widgets.button import Button, ButtonStyle +from openpilot.system.ui.widgets.label import Label class SetupWidget(Widget): @@ -13,12 +16,16 @@ class SetupWidget(Widget): super().__init__() self._open_settings_callback = None self._pairing_dialog: PairingDialog | None = None + self._pair_device_btn = Button(lambda: tr("Pair device"), self._show_pairing, button_style=ButtonStyle.PRIMARY) + self._open_settings_btn = Button(lambda: tr("Open"), lambda: self._open_settings_callback() if self._open_settings_callback else None, + button_style=ButtonStyle.PRIMARY) + self._firehose_label = Label(lambda: tr("🔥 Firehose Mode 🔥"), font_weight=FontWeight.MEDIUM, font_size=64) def set_open_settings_callback(self, callback): self._open_settings_callback = callback def _render(self, rect: rl.Rectangle): - if ui_state.prime_state.get_type() == PrimeType.UNPAIRED: + if not ui_state.prime_state.is_paired(): self._render_registration(rect) else: self._render_firehose_prompt(rect) @@ -26,7 +33,7 @@ class SetupWidget(Widget): def _render_registration(self, rect: rl.Rectangle): """Render registration prompt.""" - rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 590), 0.02, 20, rl.Color(51, 51, 51, 255)) + rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, rect.height), 0.03, 20, rl.Color(51, 51, 51, 255)) x = rect.x + 64 y = rect.y + 48 @@ -34,25 +41,24 @@ class SetupWidget(Widget): # Title font = gui_app.font(FontWeight.BOLD) - rl.draw_text_ex(font, "Finish Setup", rl.Vector2(x, y), 75, 0, rl.WHITE) + rl.draw_text_ex(font, tr("Finish Setup"), rl.Vector2(x, y), 75, 0, rl.WHITE) y += 113 # 75 + 38 spacing # Description - desc = "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer." - light_font = gui_app.font(FontWeight.LIGHT) + desc = tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.") + light_font = gui_app.font(FontWeight.NORMAL) wrapped = wrap_text(light_font, desc, 50, int(w)) for line in wrapped: rl.draw_text_ex(light_font, line, rl.Vector2(x, y), 50, 0, rl.WHITE) - y += 50 + y += 50 * FONT_SCALE - button_rect = rl.Rectangle(x, y + 50, w, 128) - if gui_button(button_rect, "Pair device", button_style=ButtonStyle.PRIMARY): - self._show_pairing() + button_rect = rl.Rectangle(x, y + 30, w, 200) + self._pair_device_btn.render(button_rect) def _render_firehose_prompt(self, rect: rl.Rectangle): """Render firehose prompt widget.""" - rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 450), 0.02, 20, rl.Color(51, 51, 51, 255)) + rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 500), 0.04, 20, rl.Color(51, 51, 51, 255)) # Content margins (56, 40, 56, 40) x = rect.x + 56 @@ -61,30 +67,31 @@ class SetupWidget(Widget): spacing = 42 # Title with fire emojis - title_font = gui_app.font(FontWeight.MEDIUM) - title_text = "Firehose Mode" - rl.draw_text_ex(title_font, title_text, rl.Vector2(x, y), 64, 0, rl.WHITE) + self._firehose_label.render(rl.Rectangle(rect.x, y, rect.width, 64)) y += 64 + spacing # Description desc_font = gui_app.font(FontWeight.NORMAL) - desc_text = "Maximize your training data uploads to improve openpilot's driving models." + desc_text = tr("Maximize your training data uploads to improve openpilot's driving models.") wrapped_desc = wrap_text(desc_font, desc_text, 40, int(w)) for line in wrapped_desc: rl.draw_text_ex(desc_font, line, rl.Vector2(x, y), 40, 0, rl.WHITE) - y += 40 + y += 40 * FONT_SCALE y += spacing # Open button button_height = 48 + 64 # font size + padding button_rect = rl.Rectangle(x, y, w, button_height) - if gui_button(button_rect, "Open", button_style=ButtonStyle.PRIMARY): - if self._open_settings_callback: - self._open_settings_callback() + self._open_settings_btn.render(button_rect) def _show_pairing(self): + if not system_time_valid(): + dlg = alert_dialog(tr("Please connect to Wi-Fi to complete initial pairing")) + gui_app.set_modal_overlay(dlg) + return + if not self._pairing_dialog: self._pairing_dialog = PairingDialog() gui_app.set_modal_overlay(self._pairing_dialog, lambda result: setattr(self, '_pairing_dialog', None)) diff --git a/selfdrive/ui/widgets/ssh_key.py b/selfdrive/ui/widgets/ssh_key.py index 4f4a8dcdff..88389cb053 100644 --- a/selfdrive/ui/widgets/ssh_key.py +++ b/selfdrive/ui/widgets/ssh_key.py @@ -2,13 +2,15 @@ import pyray as rl import requests import threading import copy +from collections.abc import Callable from enum import Enum from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.confirm_dialog import alert_dialog from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.list_view import ( @@ -20,11 +22,13 @@ from openpilot.system.ui.widgets.list_view import ( BUTTON_WIDTH, ) +VALUE_FONT_SIZE = 48 + class SshKeyActionState(Enum): - LOADING = "LOADING" - ADD = "ADD" - REMOVE = "REMOVE" + LOADING = tr_noop("LOADING") + ADD = tr_noop("ADD") + REMOVE = tr_noop("REMOVE") class SshKeyAction(ItemAction): @@ -34,13 +38,19 @@ class SshKeyAction(ItemAction): def __init__(self): super().__init__(self.MAX_WIDTH, True) - self._keyboard = Keyboard() + self._keyboard = Keyboard(min_text_size=1) self._params = Params() self._error_message: str = "" - self._text_font = gui_app.font(FontWeight.MEDIUM) + self._text_font = gui_app.font(FontWeight.NORMAL) + self._button = Button("", click_callback=self._handle_button_click, button_style=ButtonStyle.LIST_ACTION, + border_radius=BUTTON_BORDER_RADIUS, font_size=BUTTON_FONT_SIZE) self._refresh_state() + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + super().set_touch_valid_callback(touch_callback) + self._button.set_touch_valid_callback(touch_callback) + def _refresh_state(self): self._username = self._params.get("GithubUsername") self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD @@ -49,41 +59,34 @@ class SshKeyAction(ItemAction): # Show error dialog if there's an error if self._error_message: message = copy.copy(self._error_message) - gui_app.set_modal_overlay(lambda: alert_dialog(message)) + gui_app.set_modal_overlay(alert_dialog(message)) self._username = "" self._error_message = "" # Draw username if exists if self._username: - text_size = measure_text_cached(self._text_font, self._username, BUTTON_FONT_SIZE) + text_size = measure_text_cached(self._text_font, self._username, VALUE_FONT_SIZE) rl.draw_text_ex( self._text_font, self._username, (rect.x + rect.width - BUTTON_WIDTH - text_size.x - 30, rect.y + (rect.height - text_size.y) / 2), - BUTTON_FONT_SIZE, + VALUE_FONT_SIZE, 1.0, - rl.WHITE, + rl.Color(170, 170, 170, 255), ) # Draw button - if gui_button( - rl.Rectangle( - rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT - ), - self._state.value, - is_enabled=self._state != SshKeyActionState.LOADING, - border_radius=BUTTON_BORDER_RADIUS, - font_size=BUTTON_FONT_SIZE, - button_style=ButtonStyle.LIST_ACTION, - ): - self._handle_button_click() - return True + button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT) + self._button.set_rect(button_rect) + self._button.set_text(tr(self._state.value)) + self._button.set_enabled(self._state != SshKeyActionState.LOADING) + self._button.render(button_rect) return False def _handle_button_click(self): if self._state == SshKeyActionState.ADD: - self._keyboard.clear() - self._keyboard.set_title("Enter your GitHub username") + self._keyboard.reset() + self._keyboard.set_title(tr("Enter your GitHub username")) gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit) elif self._state == SshKeyActionState.REMOVE: self._params.remove("GithubUsername") @@ -108,7 +111,7 @@ class SshKeyAction(ItemAction): response.raise_for_status() keys = response.text.strip() if not keys: - raise requests.exceptions.HTTPError("No SSH keys found") + raise requests.exceptions.HTTPError(tr("No SSH keys found")) # Success - save keys self._params.put("GithubUsername", username) @@ -117,12 +120,12 @@ class SshKeyAction(ItemAction): self._username = username except requests.exceptions.Timeout: - self._error_message = "Request timed out" + self._error_message = tr("Request timed out") self._state = SshKeyActionState.ADD except Exception: - self._error_message = f"No SSH keys found for user '{username}'" + self._error_message = tr("No SSH keys found for user '{}'").format(username) self._state = SshKeyActionState.ADD -def ssh_key_item(title: str, description: str): +def ssh_key_item(title: str | Callable[[], str], description: str | Callable[[], str]) -> ListItem: return ListItem(title=title, description=description, action_item=SshKeyAction()) diff --git a/sunnypilot/modeld/SConscript b/sunnypilot/modeld/SConscript index df897e3049..dc6f6700d1 100644 --- a/sunnypilot/modeld/SConscript +++ b/sunnypilot/modeld/SConscript @@ -1,10 +1,10 @@ import glob -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') +Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations') lenv = env.Clone() lenvCython = envCython.Clone() -libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] +libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread'] frameworks = [] common_src = [ @@ -54,6 +54,6 @@ lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS 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']) + thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[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 + lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, common, 'dl', 'OpenCL']) \ No newline at end of file diff --git a/sunnypilot/modeld/runners/runmodel_pyx.pyx b/sunnypilot/modeld/runners/runmodel_pyx.pyx index e224b1e4f2..d2b4485944 100644 --- a/sunnypilot/modeld/runners/runmodel_pyx.pyx +++ b/sunnypilot/modeld/runners/runmodel_pyx.pyx @@ -4,7 +4,7 @@ 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 +from openpilot.sunnypilot.modeld.models.commonmodel_pyx cimport CLMem class Runtime: CPU = USE_CPU_RUNTIME diff --git a/sunnypilot/modeld/runners/snpemodel_pyx.pyx b/sunnypilot/modeld/runners/snpemodel_pyx.pyx index d1f24d1ead..056ba9d4fe 100644 --- a/sunnypilot/modeld/runners/snpemodel_pyx.pyx +++ b/sunnypilot/modeld/runners/snpemodel_pyx.pyx @@ -6,9 +6,9 @@ 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 +from openpilot.sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from openpilot.sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from openpilot.sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/" diff --git a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx index 49de8344c0..eaabc4a776 100644 --- a/sunnypilot/modeld/runners/thneedmodel_pyx.pyx +++ b/sunnypilot/modeld/runners/thneedmodel_pyx.pyx @@ -5,9 +5,9 @@ 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 +from openpilot.sunnypilot.modeld.models.commonmodel_pyx cimport CLContext +from openpilot.sunnypilot.modeld.runners.runmodel_pyx cimport RunModel +from openpilot.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): diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index bb4c8d4a50..4c04b7382f 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -1,10 +1,10 @@ import glob -Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations') +Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations') lenv = env.Clone() lenvCython = envCython.Clone() -libs = [cereal, messaging, visionipc, gpucommon, common, 'capnp', 'kj', 'pthread'] +libs = [cereal, messaging, visionipc, common, 'capnp', 'kj', 'pthread'] frameworks = [] common_src = [ diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 0c14315ff8..1e451a7e3e 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -11,7 +11,7 @@ import requests from requests.exceptions import (SSLError, RequestException, HTTPError) from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from sunnypilot.models.helpers import is_bundle_version_compatible +from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible from cereal import custom diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index a3ce046a33..c236643a06 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -16,8 +16,8 @@ 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 +from openpilot.sunnypilot.models.fetcher import ModelFetcher +from openpilot.sunnypilot.models.helpers import verify_file, get_active_bundle class ModelManagerSP: diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index 33d7d86e28..82ff797b5b 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -70406ab4dd66d0e384734a8a56632ae4a62bc9670c2e630a0f71588c4e212cd8 \ No newline at end of file +030a2a502e95e51290bb1d76795013b72b25521a572c3942a232b9395e544250 \ No newline at end of file diff --git a/sunnypilot/models/tests/test_tinygrad_ref.py b/sunnypilot/models/tests/test_tinygrad_ref.py index c4f98a2cb6..3e60ab5308 100644 --- a/sunnypilot/models/tests/test_tinygrad_ref.py +++ b/sunnypilot/models/tests/test_tinygrad_ref.py @@ -1,7 +1,7 @@ import requests -from sunnypilot.models.tinygrad_ref import get_tinygrad_ref -from sunnypilot.models.fetcher import ModelFetcher +from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref +from openpilot.sunnypilot.models.fetcher import ModelFetcher def fetch_tinygrad_ref(): diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index 2cfbd1eec8..b534b7e37e 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -15,7 +15,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import set_ import openpilot.system.sentry as sentry -from sunnypilot.sunnylink.statsd import STATSLOGSP +from openpilot.sunnypilot.sunnylink.statsd import STATSLOGSP def log_fingerprint(CP: structs.CarParams) -> None: diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index 609d2f78b6..31c2182d41 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -14,6 +14,13 @@ from openpilot.selfdrive.modeld.constants import ModelConstants LAT_PLAN_MIN_IDX = 5 LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan +# from selfdrive/controls/lib/latcontrol_torque.py +KP = 1.0 +KI = 0.3 +KD = 0.0 +INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] +KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] + def get_predicted_lateral_jerk(lat_accels, t_diffs): # compute finite difference between subsequent model_v2.acceleration.y values @@ -48,7 +55,6 @@ class LatControlTorqueExtBase: self.model_v2 = None self.model_valid = False self.lac_torque = lac_torque - self.torque_params = lac_torque.torque_params self.actual_lateral_jerk: float = 0.0 self.lateral_jerk_setpoint: float = 0.0 @@ -58,8 +64,7 @@ class LatControlTorqueExtBase: self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space() self._ff = 0.0 - self._pid = PIDController(self.torque_params.kp, self.torque_params.ki, - k_f=self.torque_params.kf) + self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD) self._pid_log = None self._setpoint = 0.0 self._measurement = 0.0 diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 1738a11e49..2db88299ca 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -75,14 +75,14 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): def update_feedforward_torque_space(self, CS): torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._setpoint, self._roll_compensation, CS.vEgo, CS.aEgo), - self.torque_params, gravity_adjusted=False) + self.lac_torque.torque_params, gravity_adjusted=False) torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo), - self.torque_params, gravity_adjusted=False) + self.lac_torque.torque_params, gravity_adjusted=False) self._pid_log.error = float(torque_from_setpoint - torque_from_measurement) self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation, - CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True) + CS.vEgo, CS.aEgo), self.lac_torque.torque_params, gravity_adjusted=True) self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone, - FRICTION_THRESHOLD, self.torque_params) + FRICTION_THRESHOLD, self.lac_torque.torque_params) def update_output_torque(self, CS): freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 @@ -159,6 +159,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): # apply friction override for cars with low NN friction response if self.model.friction_override: - self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.lac_torque.torque_params) self.update_output_torque(CS) 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 af7077d1c6..245b487315 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py @@ -5,6 +5,7 @@ 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.common.realtime import DT_CTRL 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 @@ -26,6 +27,6 @@ class TestNNTorqueModel: CP_SP = convert_to_capnp(CP_SP) - controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI) + controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL) assert controller.extension.has_nn_model diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py index 009e3d96af..eb6839cc19 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -9,6 +9,7 @@ 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.common.realtime import DT_CTRL 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 @@ -57,7 +58,7 @@ class TestNeuralNetworkLateralControl: CP_SP = convert_to_capnp(CP_SP) VM = VehicleModel(CP) - controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI) + controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL) torque_params = CP.lateralTuning.torque CS = car.CarState.new_message() @@ -81,7 +82,7 @@ class TestNeuralNetworkLateralControl: controller.extension.update_lateral_lag(test_lag) controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) controller.extension.update_limits() - _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True) + _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True, 0.2) assert lac_log.saturated for _ in range(1000): @@ -89,7 +90,7 @@ class TestNeuralNetworkLateralControl: controller.extension.update_lateral_lag(test_lag) controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) controller.extension.update_limits() - _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False) + _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False, 0.2) assert not lac_log.saturated for _ in range(1000): @@ -97,5 +98,5 @@ class TestNeuralNetworkLateralControl: controller.extension.update_lateral_lag(test_lag) controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) controller.extension.update_limits() - _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False) + _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False, 0.2) assert lac_log.saturated diff --git a/selfdrive/ui/sunnypilot/quiet_mode.py b/sunnypilot/selfdrive/ui/quiet_mode.py similarity index 99% rename from selfdrive/ui/sunnypilot/quiet_mode.py rename to sunnypilot/selfdrive/ui/quiet_mode.py index 893df95033..739ea1392c 100644 --- a/selfdrive/ui/sunnypilot/quiet_mode.py +++ b/sunnypilot/selfdrive/ui/quiet_mode.py @@ -17,6 +17,7 @@ ALERTS_ALWAYS_PLAY = { AudibleAlert.promptRepeat, } + class QuietMode: def __init__(self): self.params = Params() diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py index cd2a358c60..330011eb78 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -34,10 +34,10 @@ class SunnylinkApi(BaseApi): 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): + def get_token(self, payload_extra=None, expiry_hours=1): # Add your additional data here additional_data = {} - return super()._get_token(expiry_hours, **additional_data) + return super()._get_token(payload_extra, expiry_hours, **additional_data) def _status_update(self, message): print(message) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index 911739c312..1e3713c7ef 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -23,8 +23,8 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce create_connection, WebSocketConnectionClosedException) import cereal.messaging as messaging -from sunnypilot.sunnylink.api import SunnylinkApi -from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string +from openpilot.sunnypilot.sunnylink.api import SunnylinkApi +from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai') HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index e52b547afe..1b3c623fc4 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/sunnypilot/sunnylink/backups/manager.py @@ -18,9 +18,9 @@ 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 -from sunnypilot.sunnylink.utils import get_param_as_byte, save_param_from_base64_encoded_string +from openpilot.sunnypilot.sunnylink.api import SunnylinkApi +from openpilot.sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder +from openpilot.sunnypilot.sunnylink.utils import get_param_as_byte, save_param_from_base64_encoded_string class OperationType(Enum): diff --git a/sunnypilot/sunnylink/backups/utils.py b/sunnypilot/sunnylink/backups/utils.py index bcf7ea8ebb..1734a7efcf 100644 --- a/sunnypilot/sunnylink/backups/utils.py +++ b/sunnypilot/sunnylink/backups/utils.py @@ -16,7 +16,7 @@ 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.sunnypilot.sunnylink.backups.AESCipher import AESCipher from openpilot.system.hardware.hw import Paths diff --git a/sunnypilot/sunnylink/registration_manager.py b/sunnypilot/sunnylink/registration_manager.py index 805d2482db..1b822e5c2d 100755 --- a/sunnypilot/sunnylink/registration_manager.py +++ b/sunnypilot/sunnylink/registration_manager.py @@ -6,7 +6,7 @@ from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog from cereal import log, messaging -from sunnypilot.sunnylink.utils import register_sunnylink +from openpilot.sunnypilot.sunnylink.utils import register_sunnylink NetworkType = log.DeviceState.NetworkType diff --git a/sunnypilot/sunnylink/statsd.py b/sunnypilot/sunnylink/statsd.py index eefff63516..70d3d58e94 100755 --- a/sunnypilot/sunnylink/statsd.py +++ b/sunnypilot/sunnylink/statsd.py @@ -17,7 +17,7 @@ from cereal.messaging import SubMaster from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.hardware import HARDWARE -from openpilot.common.file_helpers import atomic_write_in_dir +from openpilot.common.utils import atomic_write_in_dir from openpilot.system.version import get_build_metadata from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S from openpilot.system.statsd import METRIC_TYPE, StatLogSP diff --git a/sunnypilot/sunnylink/uploader.py b/sunnypilot/sunnylink/uploader.py index 117fbdc159..5949833b46 100755 --- a/sunnypilot/sunnylink/uploader.py +++ b/sunnypilot/sunnylink/uploader.py @@ -11,8 +11,8 @@ from collections.abc import Iterator from cereal import log import cereal.messaging as messaging -from sunnypilot.sunnylink.api import SunnylinkApi -from openpilot.common.file_helpers import get_upload_stream +from openpilot.sunnypilot.sunnylink.api import SunnylinkApi +from openpilot.common.utils import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity from openpilot.system.hardware.hw import Paths diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index e86a795170..fb3938d702 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -1,7 +1,7 @@ import base64 import gzip import json -from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID +from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.common.params import Params, ParamKeyType from openpilot.system.version import is_prebuilt diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 716733021a..22e15051da 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -32,7 +32,7 @@ import cereal.messaging as messaging from cereal import log from cereal.services import SERVICE_LIST from openpilot.common.api import Api -from openpilot.common.file_helpers import CallbackReader, get_upload_stream +from openpilot.common.utils import CallbackReader, get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity from openpilot.system.hardware import HARDWARE, PC diff --git a/system/athena/registration.py b/system/athena/registration.py index 2c099c4bce..c6da463bd2 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -5,7 +5,7 @@ import jwt from pathlib import Path from datetime import datetime, timedelta, UTC -from openpilot.common.api import api_get +from openpilot.common.api import api_get, get_key_pair from openpilot.common.params import Params from openpilot.common.spinner import Spinner from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert @@ -39,20 +39,17 @@ def register(show_spinner=False) -> str | None: with open(Paths.persist_root()+"/comma/dongle_id") as f: dongle_id = f.read().strip() - pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub") - if not pubkey.is_file(): + # Create registration token, in the future, this key will make JWTs directly + jwt_algo, private_key, public_key = get_key_pair() + + if not public_key: dongle_id = UNREGISTERED_DONGLE_ID - cloudlog.warning(f"missing public key: {pubkey}") + cloudlog.warning("missing public key") elif dongle_id is None: if show_spinner: spinner = Spinner() spinner.update("registering device") - # Create registration token, in the future, this key will make JWTs directly - with open(Paths.persist_root()+"/comma/id_rsa.pub") as f1, open(Paths.persist_root()+"/comma/id_rsa") as f2: - public_key = f1.read() - private_key = f2.read() - # Block until we get the imei serial = HARDWARE.get_serial() start_time = time.monotonic() @@ -72,7 +69,7 @@ def register(show_spinner=False) -> str | None: start_time = time.monotonic() while True: try: - register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, private_key, algorithm='RS256') + register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, private_key, algorithm=jwt_algo) cloudlog.info("getting pilotauth") resp = api_get("v2/pilotauth/", method='POST', timeout=15, imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) diff --git a/system/camerad/SConscript b/system/camerad/SConscript index 734f748a2a..e288c6d8b0 100644 --- a/system/camerad/SConscript +++ b/system/camerad/SConscript @@ -1,6 +1,6 @@ -Import('env', 'arch', 'messaging', 'common', 'gpucommon', 'visionipc') +Import('env', 'arch', 'messaging', 'common', 'visionipc') -libs = [common, 'OpenCL', messaging, visionipc, gpucommon] +libs = [common, 'OpenCL', messaging, visionipc] if arch != "Darwin": camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/spectra.cc', diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index 8c4602bb31..d741e13cf3 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -12,7 +12,7 @@ #include #include -#ifdef QCOM2 +#ifdef __TICI__ #include "CL/cl_ext_qcom.h" #else #define CL_PRIORITY_HINT_HIGH_QCOM NULL @@ -50,7 +50,7 @@ public: Rect ae_xywh = {}; float measured_grey_fraction = 0; - float target_grey_fraction = 0.3; + float target_grey_fraction = 0.125; float fl_pix = 0; std::unique_ptr pm; @@ -89,7 +89,7 @@ void CameraState::set_exposure_rect() { // set areas for each camera, shouldn't be changed std::vector> ae_targets = { // (Rect, F) - std::make_pair((Rect){96, 250, 1734, 524}, 567.0), // wide + std::make_pair((Rect){96, 400, 1734, 524}, 567.0), // wide std::make_pair((Rect){96, 160, 1734, 986}, 2648.0), // road std::make_pair((Rect){96, 242, 1736, 906}, 567.0) // driver }; diff --git a/system/camerad/test/test_exposure.py b/system/camerad/test/test_exposure.py index b853b0f2f2..6f89e04800 100644 --- a/system/camerad/test/test_exposure.py +++ b/system/camerad/test/test_exposure.py @@ -20,7 +20,7 @@ class TestCamerad: def _is_exposure_okay(self, i, med_mean=None): if med_mean is None: - med_mean = np.array([[0.2,0.4],[0.2,0.6]]) + med_mean = np.array([[0.18,0.3],[0.18,0.3]]) h, w = i.shape[:2] i = i[h//10:9*h//10,w//10:9*w//10] med_ex, mean_ex = med_mean diff --git a/system/hardware/base.h b/system/hardware/base.h index df9700a017..3eded659ac 100644 --- a/system/hardware/base.h +++ b/system/hardware/base.h @@ -10,7 +10,6 @@ // no-op base hw class class HardwareNone { public: - static std::string get_os_version() { return ""; } static std::string get_name() { return ""; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; } static int get_voltage() { return 0; } @@ -22,14 +21,7 @@ public: return {}; } - static void reboot() {} - static void poweroff() {} - static void set_brightness(int percent) {} static void set_ir_power(int percentage) {} - static void set_display_power(bool on) {} - - static bool get_ssh_enabled() { return false; } - static void set_ssh_enabled(bool enabled) {} static bool PC() { return false; } static bool TICI() { return false; } diff --git a/system/hardware/base.py b/system/hardware/base.py index ce97bf294d..17d0ec1614 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -232,3 +232,12 @@ class HardwareBase(ABC): def get_modem_data_usage(self): return -1, -1 + + def get_voltage(self) -> float: + return 0. + + def get_current(self) -> float: + return 0. + + def set_ir_power(self, percent: int): + pass diff --git a/system/hardware/fan_controller.py b/system/hardware/fan_controller.py index 4c7adc0a3e..365688429a 100755 --- a/system/hardware/fan_controller.py +++ b/system/hardware/fan_controller.py @@ -18,7 +18,7 @@ class TiciFanController(BaseFanController): cloudlog.info("Setting up TICI fan handler") self.last_ignition = False - self.controller = PIDController(k_p=0, k_i=4e-3, k_f=1, rate=(1 / DT_HW)) + self.controller = PIDController(k_p=0, k_i=4e-3, rate=(1 / DT_HW)) def update(self, cur_temp: float, ignition: bool) -> int: self.controller.pos_limit = 100 if ignition else 30 diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index 0144a9df03..1d1893a8c5 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -12,7 +12,7 @@ import psutil import cereal.messaging as messaging from cereal import log from cereal.services import SERVICE_LIST -from openpilot.common.dict_helpers import strip_deprecated_keys +from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW @@ -102,8 +102,8 @@ def hw_state_thread(end_event, hw_queue): modem_version = None modem_configured = False - modem_restarted = False modem_missing_count = 0 + modem_restart_count = 0 while not end_event.is_set(): # these are expensive calls. update every 10s @@ -120,16 +120,18 @@ def hw_state_thread(end_event, hw_queue): if modem_version is not None: cloudlog.event("modem version", version=modem_version) - else: - if not modem_restarted: - # TODO: we may be able to remove this with a MM update - # ModemManager's probing on startup can fail - # rarely, restart the service to probe again. - modem_missing_count += 1 - if modem_missing_count > 3: - modem_restarted = True - cloudlog.event("restarting ModemManager") - os.system("sudo systemctl restart --no-block ModemManager") + + if AGNOS and modem_restart_count < 3 and HARDWARE.get_modem_version() is None: + # TODO: we may be able to remove this with a MM update + # ModemManager's probing on startup can fail + # rarely, restart the service to probe again. + # Also, AT commands sometimes timeout resulting in ModemManager not + # trying to use this modem anymore. + modem_missing_count += 1 + if (modem_missing_count % 4) == 0: + modem_restart_count += 1 + cloudlog.event("restarting ModemManager") + os.system("sudo systemctl restart --no-block ModemManager") tx, rx = HARDWARE.get_modem_data_usage() @@ -194,6 +196,7 @@ def hardware_thread(end_event, hw_queue) -> None: should_start_prev = False in_car = False engaged_prev = False + pwrsave = False offroad_cycle_count = 0 params = Params() @@ -349,7 +352,6 @@ def hardware_thread(end_event, hw_queue) -> None: if should_start != should_start_prev or (count == 0): params.put_bool("IsEngaged", False) engaged_prev = False - HARDWARE.set_power_save(not should_start) if sm.updated['selfdriveState']: engaged = sm['selfdriveState'].enabled @@ -363,6 +365,11 @@ def hardware_thread(end_event, hw_queue) -> None: except Exception: pass + should_pwrsave = not onroad_conditions["ignition"] and msg.deviceState.screenBrightnessPercent < 1e-3 + if should_pwrsave != pwrsave or (count == 0): + HARDWARE.set_power_save(should_pwrsave) + pwrsave = should_pwrsave + if should_start: off_ts = None if started_ts is None: diff --git a/system/hardware/hw.h b/system/hardware/hw.h index bc9d17dd81..c9af7e8a33 100644 --- a/system/hardware/hw.h +++ b/system/hardware/hw.h @@ -5,7 +5,7 @@ #include "system/hardware/base.h" #include "common/util.h" -#if QCOM2 +#if __TICI__ #include "system/hardware/tici/hardware.h" #define Hardware HardwareTici #else diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index d93963cf2c..58c3d2a4e6 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,25 +1,25 @@ [ { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", - "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", - "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz", + "hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", + "hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "size": 3282256, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" + "ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", - "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", - "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz", + "hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", + "hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "size": 98124, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" + "ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c" }, { "name": "abl", @@ -34,50 +34,50 @@ }, { "name": "aop", - "url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz", - "hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", - "hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", + "url": "https://commadist.azureedge.net/agnosupdate/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz", + "hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", + "hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "size": 184364, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180" + "ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5" }, { "name": "devcfg", - "url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz", - "hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", - "hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", + "url": "https://commadist.azureedge.net/agnosupdate/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz", + "hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", + "hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "size": 40336, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f" + "ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8" }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb.img.xz", - "hash": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", - "hash_raw": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", - "size": 17442816, + "url": "https://commadist.azureedge.net/agnosupdate/boot-90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c.img.xz", + "hash": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c", + "hash_raw": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c", + "size": 17496064, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "8ed6c2796be5c5b29d64e6413b8e878d5bd1a3981d15216d2b5e84140cc4ea2a" + "ondevice_hash": "35014c39b55010ac955c10f808b088e74259147c7a8cbf989b3dff7d95a1e8ae" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img.xz", - "hash": "325414e5c9f7516b2bf0fedb6abe6682f717897a6d84ab70d5afe91a59f244e9", - "hash_raw": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", + "url": "https://commadist.azureedge.net/agnosupdate/system-8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b.img.xz", + "hash": "e9e99988d78c7287f29ad840130f65d5a11fa2301463d5298f1072399406f889", + "hash_raw": "8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "79f4f6d0b5b4a416f0f31261b430943a78e37c26d0e226e0ef412fe0eae3c727", + "ondevice_hash": "21d3726fcdd39d126c9ecf05ccc43a104c8486b929045a63bf7e3ac8a8bb7a50", "alt": { - "hash": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", - "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img", + "hash": "8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b", + "url": "https://commadist.azureedge.net/agnosupdate/system-8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b.img", "size": 4718592000 } } diff --git a/system/hardware/tici/all-partitions.json b/system/hardware/tici/all-partitions.json index ebffc01dfd..bac2dfc594 100644 --- a/system/hardware/tici/all-partitions.json +++ b/system/hardware/tici/all-partitions.json @@ -130,25 +130,25 @@ }, { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", - "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", - "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz", + "hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", + "hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6", "size": 3282256, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" + "ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", - "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", - "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz", + "hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", + "hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9", "size": 98124, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" + "ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c" }, { "name": "abl", @@ -163,14 +163,14 @@ }, { "name": "aop", - "url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz", - "hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", - "hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", + "url": "https://commadist.azureedge.net/agnosupdate/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz", + "hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", + "hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788", "size": 184364, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180" + "ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5" }, { "name": "bluetooth", @@ -207,14 +207,14 @@ }, { "name": "devcfg", - "url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz", - "hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", - "hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", + "url": "https://commadist.azureedge.net/agnosupdate/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz", + "hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", + "hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585", "size": 40336, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f" + "ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8" }, { "name": "devinfo", @@ -339,62 +339,62 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb.img.xz", - "hash": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", - "hash_raw": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", - "size": 17442816, + "url": "https://commadist.azureedge.net/agnosupdate/boot-90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c.img.xz", + "hash": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c", + "hash_raw": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c", + "size": 17496064, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "8ed6c2796be5c5b29d64e6413b8e878d5bd1a3981d15216d2b5e84140cc4ea2a" + "ondevice_hash": "35014c39b55010ac955c10f808b088e74259147c7a8cbf989b3dff7d95a1e8ae" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img.xz", - "hash": "325414e5c9f7516b2bf0fedb6abe6682f717897a6d84ab70d5afe91a59f244e9", - "hash_raw": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", + "url": "https://commadist.azureedge.net/agnosupdate/system-8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b.img.xz", + "hash": "e9e99988d78c7287f29ad840130f65d5a11fa2301463d5298f1072399406f889", + "hash_raw": "8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "79f4f6d0b5b4a416f0f31261b430943a78e37c26d0e226e0ef412fe0eae3c727", + "ondevice_hash": "21d3726fcdd39d126c9ecf05ccc43a104c8486b929045a63bf7e3ac8a8bb7a50", "alt": { - "hash": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", - "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img", + "hash": "8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b", + "url": "https://commadist.azureedge.net/agnosupdate/system-8757f4a9d2489585249970142578029ab1dfdc5851da75fd703d2376b6f2a26b.img", "size": 4718592000 } }, { "name": "userdata_90", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-b3112984d2a8534a83d2ce43d35efdd10c7d163d9699f611f0f72ad9e9cb5af9.img.xz", - "hash": "bea163e6fb6ac6224c7f32619affb5afb834cd859971b0cab6d8297dd0098f0a", - "hash_raw": "b3112984d2a8534a83d2ce43d35efdd10c7d163d9699f611f0f72ad9e9cb5af9", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-1d461d8be17827735a28c2588bb9fcad27d4b80fba15cd2740f3a04c8f29cc90.img.xz", + "hash": "763c7366049b3c0ad71bd19abbbf5c68d2c43597d4da5dafad890507ff489899", + "hash_raw": "1d461d8be17827735a28c2588bb9fcad27d4b80fba15cd2740f3a04c8f29cc90", "size": 96636764160, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "f4841c6ae3207197886e5efbd50f44cc24822680d7b785fa2d2743c657f23287" + "ondevice_hash": "90a265b8756b18caf1be4b8dc9b8b3104898170104ed87ec3274f77acc6c28e3" }, { "name": "userdata_89", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-3e63f670e4270474cec96f4da9250ee4e87e3106b0b043b7e82371e1c761e167.img.xz", - "hash": "b5458a29dd7d4a4c9b7ad77b8baa5f804142ac78d97c6668839bf2a650e32518", - "hash_raw": "3e63f670e4270474cec96f4da9250ee4e87e3106b0b043b7e82371e1c761e167", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-ec37fcfb7d707d26d5fbc64994e20cfdbb73a27eeedfe37778559824a2032a27.img.xz", + "hash": "de475b604b63fbeb1841c6564fb8eb496da46c9a9564ec73e5d7c8045fc88ebc", + "hash_raw": "ec37fcfb7d707d26d5fbc64994e20cfdbb73a27eeedfe37778559824a2032a27", "size": 95563022336, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "1dc10c542d3b019258fc08dc7dfdb49d9abad065e46d030b89bc1a2e0197f526" + "ondevice_hash": "03f6cbddc3bfbd2d0cd316d87d488434a03095c12870c8c6fe3bc4a2946ff0ef" }, { "name": "userdata_30", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-1d3885d4370974e55f0c6f567fd0344fc5ee10db067aa5810fbaf402eadb032c.img.xz", - "hash": "687d178cfc91be5d7e8aa1333405b610fdce01775b8333bd0985b81642b94eea", - "hash_raw": "1d3885d4370974e55f0c6f567fd0344fc5ee10db067aa5810fbaf402eadb032c", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-3501f34c28f0e5ffe224f192b4a3a35a00a039980ca29a5c35d31449f3e918d6.img.xz", + "hash": "5bda2cb099b14f4944b476995d84dcb943af1858a57fdd62d5920b6e7b74fb80", + "hash_raw": "3501f34c28f0e5ffe224f192b4a3a35a00a039980ca29a5c35d31449f3e918d6", "size": 32212254720, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "9ddbd1dae6ee7dc919f018364cf2f29dad138c9203c5a49aea0cbb9bf2e137e5" + "ondevice_hash": "d1da6f8d928093dec15590b5c1740c0062031d0068a11962bdb28dca2104d8c6" } ] \ No newline at end of file diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index ed8a7e7d17..8a0c066942 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -13,12 +13,6 @@ class HardwareTici : public HardwareNone { public: - static bool TICI() { return true; } - static bool AGNOS() { return true; } - static std::string get_os_version() { - return "AGNOS " + util::read_file("/VERSION"); - } - 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())); @@ -56,16 +50,6 @@ public: return serial; } - static void reboot() { std::system("sudo reboot"); } - static void poweroff() { std::system("sudo poweroff"); } - static void set_brightness(int percent) { - float max = std::stof(util::read_file("/sys/class/backlight/panel0-backlight/max_brightness")); - std::ofstream("/sys/class/backlight/panel0-backlight/brightness") << int(percent * (max / 100.0f)) << "\n"; - } - static void set_display_power(bool on) { - std::ofstream("/sys/class/backlight/panel0-backlight/bl_power") << (on ? "0" : "4") << "\n"; - } - static void set_ir_power(int percent) { auto device = get_device_type(); if (device == cereal::InitData::DeviceType::TICI || @@ -104,7 +88,4 @@ public: return ret; } - - static bool get_ssh_enabled() { return Params().getBool("SshEnabled"); } - static void set_ssh_enabled(bool enabled) { Params().putBool("SshEnabled", enabled); } }; diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 0f50acdc38..9791f15f34 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -116,6 +116,26 @@ class Tici(HardwareBase): def get_serial(self): return self.get_cmdline()['androidboot.serialno'] + def get_voltage(self): + with open("/sys/class/hwmon/hwmon1/in1_input") as f: + return int(f.read()) + + def get_current(self): + with open("/sys/class/hwmon/hwmon1/curr1_input") as f: + return int(f.read()) + + def set_ir_power(self, percent: int): + if self.get_device_type() in ("tici", "tizi"): + return + + value = int((percent / 100) * 300) + with open("/sys/class/leds/led:switch_2/brightness", "w") as f: + f.write("0\n") + with open("/sys/class/leds/led:torch_2/brightness", "w") as f: + f.write(f"{value}\n") + with open("/sys/class/leds/led:switch_2/brightness", "w") as f: + f.write(f"{value}\n") + def get_network_type(self): try: primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) @@ -399,12 +419,13 @@ class Tici(HardwareBase): sudo_write("f", "/proc/irq/default_smp_affinity") # move these off the default core - affine_irq(1, "msm_drm") # display affine_irq(1, "msm_vidc") # encoders affine_irq(1, "i2c_geni") # sensors # *** GPU config *** # https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216 + affine_irq(5, "fts_ts") # touch + affine_irq(5, "msm_drm") # display sudo_write("1", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on") diff --git a/system/hardware/tici/updater b/system/hardware/tici/updater index 23cdc140f4..69ce323a10 100755 --- a/system/hardware/tici/updater +++ b/system/hardware/tici/updater @@ -1,3 +1,17 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eba5f44e6a763e1f74d1c718993218adcc72cba4caafe99b595fa701151a4c54 -size 10448792 +#!/usr/bin/env bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + +AGNOS_PY=$1 +MANIFEST=$2 + +if [[ ! -f "$AGNOS_PY" || ! -f "$MANIFEST" ]]; then + echo "invalid args" + exit 1 +fi + +if systemctl is-active --quiet weston-ready; then + $DIR/updater_weston $AGNOS_PY $MANIFEST +else + $DIR/updater_magic $AGNOS_PY $MANIFEST +fi diff --git a/system/hardware/tici/updater_magic b/system/hardware/tici/updater_magic new file mode 100755 index 0000000000..b4dfa9be2e --- /dev/null +++ b/system/hardware/tici/updater_magic @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7990262878becdf2eaed40ffcc96835a6fc6bc4bdf52f4df88e8b6fcadd1bff8 +size 13664323 diff --git a/system/hardware/tici/updater_weston b/system/hardware/tici/updater_weston new file mode 100755 index 0000000000..23cdc140f4 --- /dev/null +++ b/system/hardware/tici/updater_weston @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eba5f44e6a763e1f74d1c718993218adcc72cba4caafe99b595fa701151a4c54 +size 10448792 diff --git a/system/loggerd/encoderd.cc b/system/loggerd/encoderd.cc index 3237d13074..9d4b81a3f9 100644 --- a/system/loggerd/encoderd.cc +++ b/system/loggerd/encoderd.cc @@ -3,7 +3,7 @@ #include "system/loggerd/loggerd.h" #include "system/loggerd/encoder/jpeg_encoder.h" -#ifdef QCOM2 +#ifdef __TICI__ #include "system/loggerd/encoder/v4l_encoder.h" #define Encoder V4LEncoder #else diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 967caec867..8e3a74d2d9 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "cereal/messaging/messaging.h" @@ -46,7 +47,8 @@ struct EncoderSettings { } static EncoderSettings StreamEncoderSettings() { - return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 1'000'000, .gop_size = 15}; + int _stream_bitrate = getenv("STREAM_BITRATE") ? atoi(getenv("STREAM_BITRATE")) : 1'000'000; + return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = _stream_bitrate , .gop_size = 15}; } }; diff --git a/system/loggerd/uploader.py b/system/loggerd/uploader.py index 38fc0e9209..5b6234e1d5 100755 --- a/system/loggerd/uploader.py +++ b/system/loggerd/uploader.py @@ -12,7 +12,7 @@ from collections.abc import Iterator from cereal import log import cereal.messaging as messaging from openpilot.common.api import Api -from openpilot.common.file_helpers import get_upload_stream +from openpilot.common.utils import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity from openpilot.system.hardware.hw import Paths @@ -29,7 +29,7 @@ MAX_UPLOAD_SIZES = { "qcam": 5*1e6, } -allow_sleep = bool(os.getenv("UPLOADER_SLEEP", "1")) +allow_sleep = bool(int(os.getenv("UPLOADER_SLEEP", "1"))) force_wifi = os.getenv("FORCEWIFI") is not None fake_upload = os.getenv("FAKEUPLOAD") is not None diff --git a/system/manager/build.py b/system/manager/build.py index ae130f4b04..c88befd454 100755 --- a/system/manager/build.py +++ b/system/manager/build.py @@ -14,7 +14,7 @@ from openpilot.system.version import get_build_metadata 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 = 3800 +TOTAL_SCONS_NODES = 2280 MAX_BUILD_PROGRESS = 100 def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: diff --git a/system/manager/process.py b/system/manager/process.py index 5e86e87c76..e6b6a44c40 100644 --- a/system/manager/process.py +++ b/system/manager/process.py @@ -1,7 +1,6 @@ import importlib import os import signal -import struct import time import subprocess from collections.abc import Callable, ValuesView @@ -16,9 +15,6 @@ import openpilot.system.sentry as sentry from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.common.watchdog import WATCHDOG_FN - -ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None def launcher(proc: str, name: str) -> None: @@ -70,10 +66,6 @@ class ManagerProcess(ABC): proc: Process | None = None enabled = True name = "" - - last_watchdog_time = 0 - watchdog_max_dt: int | None = None - watchdog_seen = False shutting_down = False @abstractmethod @@ -88,26 +80,6 @@ class ManagerProcess(ABC): self.stop(sig=signal.SIGKILL) self.start() - def check_watchdog(self, started: bool) -> None: - if self.watchdog_max_dt is None or self.proc is None: - return - - try: - fn = WATCHDOG_FN + str(self.proc.pid) - with open(fn, "rb") as f: - self.last_watchdog_time = struct.unpack('Q', f.read())[0] - except Exception: - pass - - dt = time.monotonic() - self.last_watchdog_time / 1e9 - - if dt > self.watchdog_max_dt: - if self.watchdog_seen and ENABLE_WATCHDOG: - cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})") - self.restart() - else: - self.watchdog_seen = True - def stop(self, retry: bool = True, block: bool = True, sig: signal.Signals = None) -> int | None: if self.proc is None: return None @@ -167,14 +139,13 @@ class ManagerProcess(ABC): class NativeProcess(ManagerProcess): - def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False, watchdog_max_dt=None): + def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False): self.name = name self.cwd = cwd self.cmdline = cmdline self.should_run = should_run self.enabled = enabled self.sigkill = sigkill - self.watchdog_max_dt = watchdog_max_dt self.launcher = nativelauncher def prepare(self) -> None: @@ -192,18 +163,16 @@ class NativeProcess(ManagerProcess): cloudlog.info(f"starting process {self.name}") self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name)) self.proc.start() - self.watchdog_seen = False self.shutting_down = False class PythonProcess(ManagerProcess): - def __init__(self, name, module, should_run, enabled=True, sigkill=False, watchdog_max_dt=None): + def __init__(self, name, module, should_run, enabled=True, sigkill=False): self.name = name self.module = module self.should_run = should_run self.enabled = enabled self.sigkill = sigkill - self.watchdog_max_dt = watchdog_max_dt self.launcher = launcher def prepare(self) -> None: @@ -226,7 +195,6 @@ class PythonProcess(ManagerProcess): cloudlog.info(f"starting python {self.module}") self.proc = Process(name=name, target=self.launcher, args=(self.module, self.name)) self.proc.start() - self.watchdog_seen = False self.shutting_down = False @@ -288,8 +256,6 @@ def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None else: p.stop(block=False) - p.check_watchdog(started) - for p in running: p.start() diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 6e52635a64..7e4edc6863 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -10,8 +10,8 @@ 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 +from openpilot.sunnypilot.models.helpers import get_active_model_runner +from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -126,8 +126,7 @@ procs = [ PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)), PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC), - NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), - PythonProcess("raylib_ui", "selfdrive.ui.ui", always_run, enabled=False, watchdog_max_dt=(5 if not PC else None)), + PythonProcess("ui", "selfdrive.ui.ui", always_run), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False), diff --git a/system/micd.py b/system/micd.py index 02ef82390b..9b3ccc8d29 100755 --- a/system/micd.py +++ b/system/micd.py @@ -5,7 +5,7 @@ import threading from cereal import messaging from openpilot.common.realtime import Ratekeeper -from openpilot.common.retry import retry +from openpilot.common.utils import retry from openpilot.common.swaglog import cloudlog RATE = 10 @@ -94,7 +94,7 @@ class Mic: self.measurements = self.measurements[FFT_SAMPLES:] - @retry(attempts=7, delay=3) + @retry(attempts=10, delay=3) def get_stream(self, sd): # reload sounddevice to reinitialize portaudio sd._terminate() diff --git a/system/qcomgpsd/qcomgpsd.py b/system/qcomgpsd/qcomgpsd.py index 819b17f113..59f5ac0b50 100755 --- a/system/qcomgpsd/qcomgpsd.py +++ b/system/qcomgpsd/qcomgpsd.py @@ -16,7 +16,7 @@ from struct import unpack_from, calcsize, pack from cereal import log import cereal.messaging as messaging from openpilot.common.gpio import gpio_init, gpio_set -from openpilot.common.retry import retry +from openpilot.common.utils import retry from openpilot.common.time_helpers import system_time_valid from openpilot.system.hardware.tici.pins import GPIO from openpilot.common.swaglog import cloudlog diff --git a/system/statsd.py b/system/statsd.py index 89ffa0d6fc..17a5e4190e 100755 --- a/system/statsd.py +++ b/system/statsd.py @@ -17,7 +17,7 @@ from cereal.messaging import SubMaster from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.hardware import HARDWARE -from openpilot.common.file_helpers import atomic_write_in_dir +from openpilot.common.utils import atomic_write_in_dir from openpilot.system.version import get_build_metadata from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S diff --git a/system/ubloxd/ubloxd.py b/system/ubloxd/ubloxd.py index 84a926dd78..6882ad0955 100755 --- a/system/ubloxd/ubloxd.py +++ b/system/ubloxd/ubloxd.py @@ -498,7 +498,7 @@ def main(): sock = messaging.sub_sock('ubloxRaw', timeout=100, conflate=False) while True: - msg = messaging.recv_one_or_none(sock) + msg = messaging.recv_one(sock) if msg is None: continue diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 3f433e1fcb..e9f5484a17 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -2,8 +2,11 @@ import atexit import cffi import os import time +import signal +import sys import pyray as rl import threading +from contextlib import contextmanager from collections.abc import Callable from collections import deque from dataclasses import dataclass @@ -12,38 +15,50 @@ from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog from openpilot.system.hardware import HARDWARE, PC +from openpilot.system.ui.lib.multilang import multilang from openpilot.common.realtime import Ratekeeper -DEFAULT_FPS = int(os.getenv("FPS", "60")) +_DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20}.get(HARDWARE.get_device_type(), 60))) FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz MAX_TOUCH_SLOTS = 2 +TOUCH_HISTORY_TIMEOUT = 3.0 # Seconds before touch points fade out ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1" SHOW_FPS = os.getenv("SHOW_FPS") == "1" SHOW_TOUCHES = os.getenv("SHOW_TOUCHES") == "1" STRICT_MODE = os.getenv("STRICT_MODE") == "1" SCALE = float(os.getenv("SCALE", "1.0")) +PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0")) +PROFILE_STATS = int(os.getenv("PROFILE_STATS", "100")) # Number of functions to show in profile output DEFAULT_TEXT_SIZE = 60 DEFAULT_TEXT_COLOR = rl.WHITE +# Qt draws fonts accounting for ascent/descent differently, so compensate to match old styles +# The real scales for the fonts below range from 1.212 to 1.266 +FONT_SCALE = 1.242 + ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets") FONT_DIR = ASSETS_DIR.joinpath("fonts") class FontWeight(StrEnum): - THIN = "Inter-Thin.ttf" - EXTRA_LIGHT = "Inter-ExtraLight.ttf" - LIGHT = "Inter-Light.ttf" - NORMAL = "Inter-Regular.ttf" - MEDIUM = "Inter-Medium.ttf" - SEMI_BOLD = "Inter-SemiBold.ttf" - BOLD = "Inter-Bold.ttf" - EXTRA_BOLD = "Inter-ExtraBold.ttf" - BLACK = "Inter-Black.ttf" + LIGHT = "Inter-Light.fnt" + NORMAL = "Inter-Regular.fnt" + MEDIUM = "Inter-Medium.fnt" + SEMI_BOLD = "Inter-SemiBold.fnt" + BOLD = "Inter-Bold.fnt" + UNIFONT = "unifont.fnt" + + +def font_fallback(font: rl.Font) -> rl.Font: + """Fall back to unifont for languages that require it.""" + if multilang.requires_unifont(): + return gui_app.font(FontWeight.UNIFONT) + return font @dataclass @@ -57,6 +72,12 @@ class MousePos(NamedTuple): y: float +class MousePosWithTime(NamedTuple): + x: float + y: float + t: float + + class MouseEvent(NamedTuple): pos: MousePos slot: int @@ -72,7 +93,7 @@ class MouseState: self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS - self._rk = Ratekeeper(MOUSE_THREAD_RATE) + self._rk = Ratekeeper(MOUSE_THREAD_RATE, print_delay_threshold=None) self._lock = threading.Lock() self._exit_event = threading.Event() self._thread = None @@ -108,8 +129,8 @@ class MouseState: ev = MouseEvent( MousePos(x, y), slot, - rl.is_mouse_button_pressed(slot), - rl.is_mouse_button_released(slot), + rl.is_mouse_button_pressed(slot), # noqa: TID251 + rl.is_mouse_button_released(slot), # noqa: TID251 rl.is_mouse_button_down(slot), time.monotonic(), ) @@ -125,94 +146,177 @@ class GuiApplication: self._fonts: dict[FontWeight, rl.Font] = {} self._width = width self._height = height - self._scale = SCALE + + if PC and os.getenv("SCALE") is None: + self._scale = self._calculate_auto_scale() + else: + self._scale = SCALE + self._scaled_width = int(self._width * self._scale) self._scaled_height = int(self._height * self._scale) self._render_texture: rl.RenderTexture | None = None self._textures: dict[str, rl.Texture] = {} - self._target_fps: int = DEFAULT_FPS + self._target_fps: int = _DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() + self._frame = 0 self._window_close_requested = False self._trace_log_callback = None self._modal_overlay = ModalOverlay() + self._modal_overlay_shown = False self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] + self._last_mouse_event: MouseEvent = MouseEvent(MousePos(0, 0), 0, False, False, False, 0.0) + + self._should_render = True # Debug variables - self._mouse_history: deque[MousePos] = deque(maxlen=MOUSE_THREAD_RATE) + self._mouse_history: deque[MousePosWithTime] = deque(maxlen=MOUSE_THREAD_RATE) + self._show_touches = SHOW_TOUCHES + self._show_fps = SHOW_FPS + self._profile_render_frames = PROFILE_RENDER + self._render_profiler = None + self._render_profile_start_time = None + + @property + def frame(self): + return self._frame + + def set_show_touches(self, show: bool): + self._show_touches = show + + def set_show_fps(self, show: bool): + self._show_fps = show + + @property + def target_fps(self): + return self._target_fps def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = DEFAULT_FPS): - atexit.register(self.close) # Automatically call close() on exit + def init_window(self, title: str, fps: int = _DEFAULT_FPS): + with self._startup_profile_context(): + def _close(sig, frame): + self.close() + sys.exit(0) + signal.signal(signal.SIGINT, _close) + atexit.register(self.close) - HARDWARE.set_display_power(True) - HARDWARE.set_screen_brightness(65) + self._set_log_callback() + rl.set_trace_log_level(rl.TraceLogLevel.LOG_WARNING) - self._set_log_callback() - rl.set_trace_log_level(rl.TraceLogLevel.LOG_ALL) + flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT + if ENABLE_VSYNC: + flags |= rl.ConfigFlags.FLAG_VSYNC_HINT + rl.set_config_flags(flags) - flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT - if ENABLE_VSYNC: - flags |= rl.ConfigFlags.FLAG_VSYNC_HINT - rl.set_config_flags(flags) + rl.init_window(self._scaled_width, self._scaled_height, title) + if self._scale != 1.0: + rl.set_mouse_scale(1 / self._scale, 1 / self._scale) + self._render_texture = rl.load_render_texture(self._width, self._height) + rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) + rl.set_target_fps(fps) - rl.init_window(self._scaled_width, self._scaled_height, title) - if self._scale != 1.0: - rl.set_mouse_scale(1 / self._scale, 1 / self._scale) - self._render_texture = rl.load_render_texture(self._width, self._height) - rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) - rl.set_target_fps(fps) + self._target_fps = fps + self._set_styles() + self._load_fonts() + self._patch_text_functions() - self._target_fps = fps - self._set_styles() - self._load_fonts() + if not PC: + self._mouse.start() - if not PC: - self._mouse.start() + @contextmanager + def _startup_profile_context(self): + if "PROFILE_STARTUP" not in os.environ: + yield + return + + import cProfile + import io + import pstats + + profiler = cProfile.Profile() + start_time = time.monotonic() + profiler.enable() + + # do the init + yield + + profiler.disable() + elapsed_ms = (time.monotonic() - start_time) * 1e3 + + stats_stream = io.StringIO() + pstats.Stats(profiler, stream=stats_stream).sort_stats("cumtime").print_stats(25) + print("\n=== Startup profile ===") + print(stats_stream.getvalue().rstrip()) + + green = "\033[92m" + reset = "\033[0m" + print(f"{green}UI window ready in {elapsed_ms:.1f} ms{reset}") + sys.exit(0) def set_modal_overlay(self, overlay, callback: Callable | None = None): + if self._modal_overlay.overlay is not None: + if self._modal_overlay.callback is not None: + self._modal_overlay.callback(-1) + self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) - def texture(self, asset_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True): + def set_should_render(self, should_render: bool): + self._should_render = should_render + + def texture(self, asset_path: str, width: int | None = None, height: int | None = None, + 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.joinpath(asset_path)) as fspath: - texture_obj = self._load_texture_from_image(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + image_obj = self._load_image_from_path(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + texture_obj = self._load_texture_from_image(image_obj) self._textures[cache_key] = texture_obj return texture_obj - def _load_texture_from_image(self, image_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True): - """Load and resize a texture, storing it for later automatic unloading.""" + def _load_image_from_path(self, image_path: str, width: int | None = None, height: int | None = None, + alpha_premultiply: bool = False, keep_aspect_ratio: bool = True) -> rl.Image: + """Load and resize an image, storing it for later automatic unloading.""" image = rl.load_image(image_path) if alpha_premultiply: rl.image_alpha_premultiply(image) - # Resize with aspect ratio preservation if requested - if keep_aspect_ratio: - orig_width = image.width - orig_height = image.height + if width is not None and height is not None: + same_dimensions = image.width == width and image.height == height - scale_width = width / orig_width - scale_height = height / orig_height + # Resize with aspect ratio preservation if requested + if not same_dimensions: + if keep_aspect_ratio: + orig_width = image.width + orig_height = image.height - # Calculate new dimensions - scale = min(scale_width, scale_height) - new_width = int(orig_width * scale) - new_height = int(orig_height * scale) + scale_width = width / orig_width + scale_height = height / orig_height - rl.image_resize(image, new_width, new_height) + # Calculate new dimensions + scale = min(scale_width, scale_height) + new_width = int(orig_width * scale) + new_height = int(orig_height * scale) + + rl.image_resize(image, new_width, new_height) + else: + rl.image_resize(image, width, height) else: - rl.image_resize(image, width, height) + assert keep_aspect_ratio, "Cannot resize without specifying width and height" + return image + def _load_texture_from_image(self, image: rl.Image) -> rl.Texture: + """Send image to GPU and unload original image.""" texture = rl.load_texture_from_image(image) # Set texture filtering to smooth the result rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) + # prevent artifacts from wrapping coordinates + rl.set_texture_wrap(texture, rl.TextureWrap.TEXTURE_WRAP_CLAMP) rl.unload_image(image) return texture @@ -242,8 +346,18 @@ class GuiApplication: def mouse_events(self) -> list[MouseEvent]: return self._mouse_events + @property + def last_mouse_event(self) -> MouseEvent: + return self._last_mouse_event + def render(self): try: + if self._profile_render_frames > 0: + import cProfile + self._render_profiler = cProfile.Profile() + self._render_profile_start_time = time.monotonic() + self._render_profiler.enable() + while not (self._window_close_requested or rl.window_should_close()): if PC: # Thread is not used on PC, need to manually add mouse events @@ -251,6 +365,16 @@ class GuiApplication: # Store all mouse events for the current frame self._mouse_events = self._mouse.get_events() + if len(self._mouse_events) > 0: + self._last_mouse_event = self._mouse_events[-1] + + # Skip rendering when screen is off + if not self._should_render: + if PC: + rl.poll_input_events() + time.sleep(1 / self._target_fps) + yield False + continue if self._render_texture: rl.begin_texture_mode(self._render_texture) @@ -260,22 +384,10 @@ class GuiApplication: rl.clear_background(rl.BLACK) # Handle modal overlay rendering and input processing - if self._modal_overlay.overlay: - if hasattr(self._modal_overlay.overlay, 'render'): - result = self._modal_overlay.overlay.render(rl.Rectangle(0, 0, self.width, self.height)) - elif callable(self._modal_overlay.overlay): - result = self._modal_overlay.overlay() - else: - raise Exception - - if result >= 0: - # Execute callback with the result and clear the overlay - if self._modal_overlay.callback is not None: - self._modal_overlay.callback(result) - - self._modal_overlay = ModalOverlay() + if self._handle_modal_overlay(): + yield False else: - yield + yield True if self._render_texture: rl.end_texture_mode() @@ -285,29 +397,22 @@ class GuiApplication: dst_rect = rl.Rectangle(0, 0, float(self._scaled_width), float(self._scaled_height)) rl.draw_texture_pro(self._render_texture.texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE) - if SHOW_FPS: + if self._show_fps: rl.draw_fps(10, 10) - if SHOW_TOUCHES: - for mouse_event in self._mouse_events: - if mouse_event.left_pressed: - self._mouse_history.clear() - self._mouse_history.append(mouse_event.pos) - - if self._mouse_history: - mouse_pos = self._mouse_history[-1] - rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 15, rl.RED) - for idx, mouse_pos in enumerate(self._mouse_history): - perc = idx / len(self._mouse_history) - color = rl.Color(min(int(255 * (1.5 - perc)), 255), int(min(255 * (perc + 0.5), 255)), 50, 255) - rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 5, color) + if self._show_touches: + self._draw_touch_points() rl.end_drawing() self._monitor_fps() + self._frame += 1 + + if self._profile_render_frames > 0 and self._frame >= self._profile_render_frames: + self._output_render_profile() except KeyboardInterrupt: pass - def font(self, font_weight: FontWeight = FontWeight.NORMAL): + def font(self, font_weight: FontWeight = FontWeight.NORMAL) -> rl.Font: return self._fonts[font_weight] @property @@ -318,26 +423,39 @@ class GuiApplication: def height(self): return self._height + def _handle_modal_overlay(self) -> bool: + if self._modal_overlay.overlay: + if hasattr(self._modal_overlay.overlay, 'render'): + result = self._modal_overlay.overlay.render(rl.Rectangle(0, 0, self.width, self.height)) + elif callable(self._modal_overlay.overlay): + result = self._modal_overlay.overlay() + else: + raise Exception + + # Send show event to Widget + if not self._modal_overlay_shown and hasattr(self._modal_overlay.overlay, 'show_event'): + self._modal_overlay.overlay.show_event() + self._modal_overlay_shown = True + + if result >= 0: + # Clear the overlay and execute the callback + original_modal = self._modal_overlay + self._modal_overlay = ModalOverlay() + if original_modal.callback is not None: + original_modal.callback(result) + return True + else: + self._modal_overlay_shown = False + return False + def _load_fonts(self): - # Create a character set from our keyboard layouts - from openpilot.system.ui.widgets.keyboard import KEYBOARD_LAYOUTS - - all_chars = set() - for layout in KEYBOARD_LAYOUTS.values(): - all_chars.update(key for row in layout for key in row) - all_chars = "".join(all_chars) - all_chars += "–✓×°" - - codepoint_count = rl.ffi.new("int *", 1) - codepoints = rl.load_codepoints(all_chars, codepoint_count) - for font_weight_file in FontWeight: - with as_file(FONT_DIR.joinpath(font_weight_file)) as fspath: - font = rl.load_font_ex(fspath.as_posix(), 200, codepoints, codepoint_count[0]) - rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) + with as_file(FONT_DIR) as fspath: + fnt_path = fspath / font_weight_file + font = rl.load_font(fnt_path.as_posix()) + if font_weight_file != FontWeight.UNIFONT: + rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) self._fonts[font_weight_file] = font - - rl.unload_codepoints(codepoints) rl.gui_set_font(self._fonts[FontWeight.NORMAL]) def _set_styles(self): @@ -347,6 +465,17 @@ class GuiApplication: rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR)) rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255))) + def _patch_text_functions(self): + # Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt + if not hasattr(rl, "_orig_draw_text_ex"): + rl._orig_draw_text_ex = rl.draw_text_ex + + def _draw_text_ex_scaled(font, text, position, font_size, spacing, tint): + font = font_fallback(font) + return rl._orig_draw_text_ex(font, text, position, font_size * FONT_SCALE, spacing, tint) + + rl.draw_text_ex = _draw_text_ex_scaled + def _set_log_callback(self): ffi_libc = cffi.FFI() ffi_libc.cdef(""" @@ -402,5 +531,56 @@ class GuiApplication: cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.") os._exit(1) + def _draw_touch_points(self): + current_time = time.monotonic() + + for mouse_event in self._mouse_events: + if mouse_event.left_pressed: + self._mouse_history.clear() + self._mouse_history.append(MousePosWithTime(mouse_event.pos.x * self._scale, mouse_event.pos.y * self._scale, current_time)) + + # Remove old touch points that exceed the timeout + while self._mouse_history and (current_time - self._mouse_history[0].t) > TOUCH_HISTORY_TIMEOUT: + self._mouse_history.popleft() + + if self._mouse_history: + mouse_pos = self._mouse_history[-1] + rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 15, rl.RED) + for idx, mouse_pos in enumerate(self._mouse_history): + perc = idx / len(self._mouse_history) + color = rl.Color(min(int(255 * (1.5 - perc)), 255), int(min(255 * (perc + 0.5), 255)), 50, 255) + rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 5, color) + + def _output_render_profile(self): + import io + import pstats + + self._render_profiler.disable() + elapsed_ms = (time.monotonic() - self._render_profile_start_time) * 1e3 + avg_frame_time = elapsed_ms / self._frame if self._frame > 0 else 0 + + stats_stream = io.StringIO() + pstats.Stats(self._render_profiler, stream=stats_stream).sort_stats("cumtime").print_stats(PROFILE_STATS) + print("\n=== Render loop profile ===") + print(stats_stream.getvalue().rstrip()) + + green = "\033[92m" + reset = "\033[0m" + print(f"\n{green}Rendered {self._frame} frames in {elapsed_ms:.1f} ms{reset}") + print(f"{green}Average frame time: {avg_frame_time:.2f} ms ({1000/avg_frame_time:.1f} FPS){reset}") + sys.exit(0) + + def _calculate_auto_scale(self) -> float: + # Create temporary window to query monitor info + rl.init_window(1, 1, "") + w, h = rl.get_monitor_width(0), rl.get_monitor_height(0) + rl.close_window() + + if w == 0 or h == 0 or (w >= self._width and h >= self._height): + return 1.0 + + # Apply 0.95 factor for window decorations/taskbar margin + return max(0.3, min(w / self._width, h / self._height) * 0.95) + gui_app = GuiApplication(2160, 1080) diff --git a/system/ui/lib/emoji.py b/system/ui/lib/emoji.py index 28139158a1..37228e2d45 100644 --- a/system/ui/lib/emoji.py +++ b/system/ui/lib/emoji.py @@ -4,6 +4,9 @@ import re from PIL import Image, ImageDraw, ImageFont import pyray as rl +from openpilot.system.ui.lib.application import FONT_DIR + +_emoji_font: ImageFont.FreeTypeFont | None = None _cache: dict[str, rl.Texture] = {} EMOJI_REGEX = re.compile( @@ -26,10 +29,16 @@ EMOJI_REGEX = re.compile( \u231a \ufe0f \u3030 -]+""", +]+""".replace("\n", ""), flags=re.UNICODE ) +def _load_emoji_font() -> ImageFont.FreeTypeFont | None: + global _emoji_font + if _emoji_font is None: + _emoji_font = ImageFont.truetype(str(FONT_DIR.joinpath("NotoColorEmoji.ttf")), 109) + return _emoji_font + def find_emoji(text): return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)] @@ -37,11 +46,10 @@ def emoji_tex(emoji): if emoji not in _cache: img = Image.new("RGBA", (128, 128), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) - font = ImageFont.truetype("NotoColorEmoji", 109) - draw.text((0, 0), emoji, font=font, embedded_color=True) - buffer = io.BytesIO() - img.save(buffer, format="PNG") - l = buffer.tell() - buffer.seek(0) - _cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l)) + draw.text((0, 0), emoji, font=_load_emoji_font(), embedded_color=True) + with io.BytesIO() as buffer: + img.save(buffer, format="PNG") + l = buffer.tell() + buffer.seek(0) + _cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l)) return _cache[emoji] diff --git a/system/ui/lib/multilang.py b/system/ui/lib/multilang.py new file mode 100644 index 0000000000..9b10a8bdc7 --- /dev/null +++ b/system/ui/lib/multilang.py @@ -0,0 +1,88 @@ +from importlib.resources import files +import os +import json +import gettext +from openpilot.common.basedir import BASEDIR +from openpilot.common.swaglog import cloudlog + +try: + from openpilot.common.params import Params +except ImportError: + Params = None + +SYSTEM_UI_DIR = os.path.join(BASEDIR, "system", "ui") +UI_DIR = files("openpilot.selfdrive.ui") +TRANSLATIONS_DIR = UI_DIR.joinpath("translations") +LANGUAGES_FILE = TRANSLATIONS_DIR.joinpath("languages.json") + +UNIFONT_LANGUAGES = [ + "ar", + "th", + "zh-CHT", + "zh-CHS", + "ko", + "ja", +] + + +class Multilang: + def __init__(self): + self._params = Params() if Params is not None else None + self._language: str = "en" + self.languages = {} + self.codes = {} + self._translation: gettext.NullTranslations | gettext.GNUTranslations = gettext.NullTranslations() + self._load_languages() + + @property + def language(self) -> str: + return self._language + + def requires_unifont(self) -> bool: + """Certain languages require unifont to render their glyphs.""" + return self._language in UNIFONT_LANGUAGES + + def setup(self): + try: + with TRANSLATIONS_DIR.joinpath(f'app_{self._language}.mo').open('rb') as fh: + translation = gettext.GNUTranslations(fh) + translation.install() + self._translation = translation + cloudlog.warning(f"Loaded translations for language: {self._language}") + except FileNotFoundError: + cloudlog.error(f"No translation file found for language: {self._language}, using default.") + gettext.install('app') + self._translation = gettext.NullTranslations() + + def change_language(self, language_code: str) -> None: + # Reinstall gettext with the selected language + self._params.put("LanguageSetting", language_code) + self._language = language_code + self.setup() + + def tr(self, text: str) -> str: + return self._translation.gettext(text) + + def trn(self, singular: str, plural: str, n: int) -> str: + return self._translation.ngettext(singular, plural, n) + + def _load_languages(self): + with LANGUAGES_FILE.open(encoding='utf-8') as f: + self.languages = json.load(f) + self.codes = {v: k for k, v in self.languages.items()} + + if self._params is not None: + lang = str(self._params.get("LanguageSetting")).removeprefix("main_") + if lang in self.codes: + self._language = lang + + +multilang = Multilang() +multilang.setup() + +tr, trn = multilang.tr, multilang.trn + + +# no-op marker for static strings translated later +def tr_noop(s: str) -> str: + return s diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py index 07b5d42f4b..ffa2ff4db9 100644 --- a/system/ui/lib/networkmanager.py +++ b/system/ui/lib/networkmanager.py @@ -21,9 +21,11 @@ NM_ACCESS_POINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings' NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings' NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection' +NM_ACTIVE_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties' -NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" +NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +NM_IP4_CONFIG_IFACE = 'org.freedesktop.NetworkManager.IP4Config' NM_DEVICE_TYPE_WIFI = 2 NM_DEVICE_TYPE_MODEM = 8 diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index e2296fd5ed..a5b9fc70d3 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -1,189 +1,134 @@ -import time +import math import pyray as rl -from collections import deque from enum import IntEnum -from openpilot.system.ui.lib.application import gui_app, MouseEvent, MousePos +from openpilot.system.ui.lib.application import gui_app, MouseEvent +from openpilot.common.filter_simple import FirstOrderFilter # Scroll constants for smooth scrolling behavior -MOUSE_WHEEL_SCROLL_SPEED = 30 -INERTIA_FRICTION = 0.92 # The rate at which the inertia slows down -MIN_VELOCITY = 0.5 # Minimum velocity before stopping the inertia -DRAG_THRESHOLD = 12 # Pixels of movement to consider it a drag, not a click -BOUNCE_FACTOR = 0.2 # Elastic bounce when scrolling past boundaries -BOUNCE_RETURN_SPEED = 0.15 # How quickly it returns from the bounce -MAX_BOUNCE_DISTANCE = 150 # Maximum distance for bounce effect -FLICK_MULTIPLIER = 1.8 # Multiplier for flick gestures -VELOCITY_HISTORY_SIZE = 5 # Track velocity over multiple frames for smoother motion +MOUSE_WHEEL_SCROLL_SPEED = 50 +BOUNCE_RETURN_RATE = 5 # ~0.92 at 60fps +MIN_VELOCITY = 2 # px/s, changes from auto scroll to steady state +MIN_VELOCITY_FOR_CLICKING = 2 * 60 # px/s, accepts clicks while auto scrolling below this velocity +DRAG_THRESHOLD = 12 # pixels of movement to consider it a drag, not a click + +DEBUG = False class ScrollState(IntEnum): - IDLE = 0 - DRAGGING_CONTENT = 1 - DRAGGING_SCROLLBAR = 2 - BOUNCING = 3 + IDLE = 0 # Not dragging, content may be bouncing or scrolling with inertia + DRAGGING_CONTENT = 1 # User is actively dragging the content class GuiScrollPanel: - def __init__(self, show_vertical_scroll_bar: bool = False): + def __init__(self): self._scroll_state: ScrollState = ScrollState.IDLE self._last_mouse_y: float = 0.0 self._start_mouse_y: float = 0.0 # Track the initial mouse position for drag detection - self._offset = rl.Vector2(0, 0) - self._view = rl.Rectangle(0, 0, 0, 0) - self._show_vertical_scroll_bar: bool = show_vertical_scroll_bar - self._velocity_y = 0.0 # Velocity for inertia - self._is_dragging: bool = False - self._bounce_offset: float = 0.0 - self._velocity_history: deque[float] = deque(maxlen=VELOCITY_HISTORY_SIZE) + self._offset_filter_y = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._velocity_filter_y = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps) self._last_drag_time: float = 0.0 - self._content_rect: rl.Rectangle | None = None - self._bounds_rect: rl.Rectangle | None = None - def handle_scroll(self, bounds: rl.Rectangle, content: rl.Rectangle) -> rl.Vector2: - # TODO: HACK: this class is driven by mouse events, so we need to ensure we have at least one event to process - for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), 0, False, False, False, time.monotonic())]: + def update(self, bounds: rl.Rectangle, content: rl.Rectangle) -> float: + for mouse_event in gui_app.mouse_events: if mouse_event.slot == 0: self._handle_mouse_event(mouse_event, bounds, content) - return self._offset - def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle): - # Store rectangles for reference - self._content_rect = content - self._bounds_rect = bounds + self._update_state(bounds, content) - max_scroll_y = max(content.height - bounds.height, 0) + return float(self._offset_filter_y.x) - # Start dragging on mouse press - if rl.check_collision_point_rec(mouse_event.pos, bounds) and mouse_event.left_pressed: - if self._scroll_state == ScrollState.IDLE or self._scroll_state == ScrollState.BOUNCING: - self._scroll_state = ScrollState.DRAGGING_CONTENT - if self._show_vertical_scroll_bar: - scrollbar_width = rl.gui_get_style(rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH) - scrollbar_x = bounds.x + bounds.width - scrollbar_width - if mouse_event.pos.x >= scrollbar_x: - self._scroll_state = ScrollState.DRAGGING_SCROLLBAR - - # TODO: hacky - # when clicking while moving, go straight into dragging - self._is_dragging = abs(self._velocity_y) > MIN_VELOCITY - self._last_mouse_y = mouse_event.pos.y - self._start_mouse_y = mouse_event.pos.y - self._last_drag_time = mouse_event.t - self._velocity_history.clear() - self._velocity_y = 0.0 - self._bounce_offset = 0.0 - - # Handle active dragging - if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR: - if mouse_event.left_down: - delta_y = mouse_event.pos.y - self._last_mouse_y - - # Track velocity for inertia - time_since_last_drag = mouse_event.t - self._last_drag_time - if time_since_last_drag > 0: - # TODO: HACK: /2 since we usually get two touch events per frame - drag_velocity = delta_y / time_since_last_drag / 60.0 / 2 # TODO: shouldn't be hardcoded - self._velocity_history.append(drag_velocity) - - self._last_drag_time = mouse_event.t - - # Detect actual dragging - total_drag = abs(mouse_event.pos.y - self._start_mouse_y) - if total_drag > DRAG_THRESHOLD: - self._is_dragging = True - - if self._scroll_state == ScrollState.DRAGGING_CONTENT: - # Add resistance at boundaries - if (self._offset.y > 0 and delta_y > 0) or (self._offset.y < -max_scroll_y and delta_y < 0): - delta_y *= BOUNCE_FACTOR - - self._offset.y += delta_y - elif self._scroll_state == ScrollState.DRAGGING_SCROLLBAR: - scroll_ratio = content.height / bounds.height - self._offset.y -= delta_y * scroll_ratio - - self._last_mouse_y = mouse_event.pos.y - - elif mouse_event.left_released: - # Calculate flick velocity - if self._velocity_history: - total_weight = 0 - weighted_velocity = 0.0 - - for i, v in enumerate(self._velocity_history): - weight = i + 1 - weighted_velocity += v * weight - total_weight += weight - - if total_weight > 0: - avg_velocity = weighted_velocity / total_weight - self._velocity_y = avg_velocity * FLICK_MULTIPLIER - - # Check bounds - if self._offset.y > 0 or self._offset.y < -max_scroll_y: - self._scroll_state = ScrollState.BOUNCING - else: - self._scroll_state = ScrollState.IDLE + def _update_state(self, bounds: rl.Rectangle, content: rl.Rectangle): + if DEBUG: + rl.draw_rectangle_lines(0, 0, abs(int(self._velocity_filter_y.x)), 10, rl.RED) # Handle mouse wheel - wheel_move = rl.get_mouse_wheel_move() - if wheel_move != 0: - self._velocity_y = 0.0 + self._offset_filter_y.x += rl.get_mouse_wheel_move() * MOUSE_WHEEL_SCROLL_SPEED - if self._show_vertical_scroll_bar: - self._offset.y += wheel_move * (MOUSE_WHEEL_SCROLL_SPEED - 20) - rl.gui_scroll_panel(bounds, rl.ffi.NULL, content, self._offset, self._view) - else: - self._offset.y += wheel_move * MOUSE_WHEEL_SCROLL_SPEED - - if self._offset.y > 0 or self._offset.y < -max_scroll_y: - self._scroll_state = ScrollState.BOUNCING - - # Apply inertia (continue scrolling after mouse release) + max_scroll_distance = max(0, content.height - bounds.height) if self._scroll_state == ScrollState.IDLE: - if abs(self._velocity_y) > MIN_VELOCITY: - self._offset.y += self._velocity_y - self._velocity_y *= INERTIA_FRICTION + above_bounds, below_bounds = self._check_bounds(bounds, content) - if self._offset.y > 0 or self._offset.y < -max_scroll_y: - self._scroll_state = ScrollState.BOUNCING + # Decay velocity when idle + if abs(self._velocity_filter_y.x) > MIN_VELOCITY: + # Faster decay if bouncing back from out of bounds + friction = math.exp(-BOUNCE_RETURN_RATE * 1 / gui_app.target_fps) + self._velocity_filter_y.x *= friction ** 2 if (above_bounds or below_bounds) else friction else: - self._velocity_y = 0.0 + self._velocity_filter_y.x = 0.0 - # Handle bouncing effect - elif self._scroll_state == ScrollState.BOUNCING: - target_y = 0.0 - if self._offset.y < -max_scroll_y: - target_y = -max_scroll_y + if above_bounds or below_bounds: + if above_bounds: + self._offset_filter_y.update(0) + else: + self._offset_filter_y.update(-max_scroll_distance) - distance = target_y - self._offset.y - bounce_step = distance * BOUNCE_RETURN_SPEED - self._offset.y += bounce_step - self._velocity_y *= INERTIA_FRICTION * 0.8 + self._offset_filter_y.x += self._velocity_filter_y.x / gui_app.target_fps - if abs(distance) < 0.5 and abs(self._velocity_y) < MIN_VELOCITY: - self._offset.y = target_y - self._velocity_y = 0.0 + elif self._scroll_state == ScrollState.DRAGGING_CONTENT: + # Mouse not moving, decay velocity + if not len(gui_app.mouse_events): + self._velocity_filter_y.update(0.0) + + # Settle to exact bounds + if abs(self._offset_filter_y.x) < 1e-2: + self._offset_filter_y.x = 0.0 + elif abs(self._offset_filter_y.x + max_scroll_distance) < 1e-2: + self._offset_filter_y.x = -max_scroll_distance + + def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle): + if self._scroll_state == ScrollState.IDLE: + if rl.check_collision_point_rec(mouse_event.pos, bounds): + if mouse_event.left_pressed: + self._start_mouse_y = mouse_event.pos.y + # Interrupt scrolling with new drag + # TODO: stop scrolling with any tap, need to fix is_touch_valid + if abs(self._velocity_filter_y.x) > MIN_VELOCITY_FOR_CLICKING: + self._scroll_state = ScrollState.DRAGGING_CONTENT + # Start velocity at initial measurement for more immediate response + self._velocity_filter_y.initialized = False + + if mouse_event.left_down: + if abs(mouse_event.pos.y - self._start_mouse_y) > DRAG_THRESHOLD: + self._scroll_state = ScrollState.DRAGGING_CONTENT + # Start velocity at initial measurement for more immediate response + self._velocity_filter_y.initialized = False + + elif self._scroll_state == ScrollState.DRAGGING_CONTENT: + if mouse_event.left_released: self._scroll_state = ScrollState.IDLE + else: + delta_y = mouse_event.pos.y - self._last_mouse_y + above_bounds, below_bounds = self._check_bounds(bounds, content) + # Rubber banding effect when out of bands + if above_bounds or below_bounds: + delta_y /= 3 - # Limit bounce distance - if self._scroll_state != ScrollState.DRAGGING_CONTENT: - if self._offset.y > MAX_BOUNCE_DISTANCE: - self._offset.y = MAX_BOUNCE_DISTANCE - elif self._offset.y < -(max_scroll_y + MAX_BOUNCE_DISTANCE): - self._offset.y = -(max_scroll_y + MAX_BOUNCE_DISTANCE) + self._offset_filter_y.x += delta_y + + # Track velocity for inertia + dt = mouse_event.t - self._last_drag_time + if dt > 0: + drag_velocity = delta_y / dt + self._velocity_filter_y.update(drag_velocity) + + # TODO: just store last mouse event! + self._last_drag_time = mouse_event.t + self._last_mouse_y = mouse_event.pos.y + + def _check_bounds(self, bounds: rl.Rectangle, content: rl.Rectangle) -> tuple[bool, bool]: + max_scroll_distance = max(0, content.height - bounds.height) + above_bounds = self._offset_filter_y.x > 0 + below_bounds = self._offset_filter_y.x < -max_scroll_distance + return above_bounds, below_bounds def is_touch_valid(self): - return not self._is_dragging + return self._scroll_state == ScrollState.IDLE and abs(self._velocity_filter_y.x) < MIN_VELOCITY_FOR_CLICKING - def get_normalized_scroll_position(self) -> float: - """Returns the current scroll position as a value from 0.0 to 1.0""" - if not self._content_rect or not self._bounds_rect: - return 0.0 + def set_offset(self, position: float) -> None: + self._offset_filter_y.x = position + self._velocity_filter_y.x = 0.0 + self._scroll_state = ScrollState.IDLE - max_scroll_y = max(self._content_rect.height - self._bounds_rect.height, 0) - if max_scroll_y == 0: - return 0.0 - - normalized = -self._offset.y / max_scroll_y - return max(0.0, min(1.0, normalized)) + @property + def offset(self) -> float: + return float(self._offset_filter_y.x) diff --git a/system/ui/lib/shader_polygon.py b/system/ui/lib/shader_polygon.py index 39bc0d5aa4..28585b08ba 100644 --- a/system/ui/lib/shader_polygon.py +++ b/system/ui/lib/shader_polygon.py @@ -1,9 +1,33 @@ import platform import pyray as rl import numpy as np -from typing import Any +from dataclasses import dataclass +from typing import Any, Optional, cast +from openpilot.system.ui.lib.application import gui_app + +MAX_GRADIENT_COLORS = 20 # includes stops as well + + +@dataclass +class Gradient: + start: tuple[float, float] + end: tuple[float, float] + colors: list[rl.Color] + stops: list[float] + + def __post_init__(self): + if len(self.colors) > MAX_GRADIENT_COLORS: + self.colors = self.colors[:MAX_GRADIENT_COLORS] + print(f"Warning: Gradient colors truncated to {MAX_GRADIENT_COLORS} entries") + + if len(self.stops) > MAX_GRADIENT_COLORS: + self.stops = self.stops[:MAX_GRADIENT_COLORS] + print(f"Warning: Gradient stops truncated to {MAX_GRADIENT_COLORS} entries") + + if not len(self.stops): + color_count = min(len(self.colors), MAX_GRADIENT_COLORS) + self.stops = [i / max(1, color_count - 1) for i in range(color_count)] -MAX_GRADIENT_COLORS = 15 VERSION = """ #version 300 es @@ -18,106 +42,43 @@ FRAGMENT_SHADER = VERSION + """ in vec2 fragTexCoord; out vec4 finalColor; -uniform vec2 points[100]; -uniform int pointCount; uniform vec4 fillColor; -uniform vec2 resolution; +// Gradient line defined in *screen pixels* uniform int useGradient; -uniform vec2 gradientStart; -uniform vec2 gradientEnd; -uniform vec4 gradientColors[15]; -uniform float gradientStops[15]; +uniform vec2 gradientStart; // e.g. vec2(0, 0) +uniform vec2 gradientEnd; // e.g. vec2(0, screenHeight) +uniform vec4 gradientColors[20]; +uniform float gradientStops[20]; uniform int gradientColorCount; -vec4 getGradientColor(vec2 pos) { - vec2 gradientDir = gradientEnd - gradientStart; - float gradientLength = length(gradientDir); - if (gradientLength < 0.001) return gradientColors[0]; +vec4 getGradientColor(vec2 p) { + // Compute t from screen-space position + vec2 d = gradientStart - gradientEnd; + float len2 = max(dot(d, d), 1e-6); + float t = clamp(dot(p - gradientEnd, d) / len2, 0.0, 1.0); - vec2 normalizedDir = gradientDir / gradientLength; - float t = clamp(dot(pos - gradientStart, normalizedDir) / gradientLength, 0.0, 1.0); + // Clamp to range + float t0 = gradientStops[0]; + float tn = gradientStops[gradientColorCount-1]; + if (t <= t0) return gradientColors[0]; + if (t >= tn) return gradientColors[gradientColorCount-1]; - if (gradientColorCount <= 1) return gradientColors[0]; - - // handle t before first / after last stop - if (t <= gradientStops[0]) return gradientColors[0]; - if (t >= gradientStops[gradientColorCount-1]) return gradientColors[gradientColorCount-1]; for (int i = 0; i < gradientColorCount - 1; i++) { - if (t >= gradientStops[i] && t <= gradientStops[i+1]) { - float segmentT = (t - gradientStops[i]) / (gradientStops[i+1] - gradientStops[i]); - return mix(gradientColors[i], gradientColors[i+1], segmentT); + float a = gradientStops[i]; + float b = gradientStops[i+1]; + if (t >= a && t <= b) { + float k = (t - a) / max(b - a, 1e-6); + return mix(gradientColors[i], gradientColors[i+1], k); } } return gradientColors[gradientColorCount-1]; } -bool isPointInsidePolygon(vec2 p) { - if (pointCount < 3) return false; - int crossings = 0; - for (int i = 0, j = pointCount - 1; i < pointCount; j = i++) { - vec2 pi = points[i]; - vec2 pj = points[j]; - if (distance(pi, pj) < 0.001) continue; - if (((pi.y > p.y) != (pj.y > p.y)) && - (p.x < (pj.x - pi.x) * (p.y - pi.y) / (pj.y - pi.y + 0.001) + pi.x)) { - crossings++; - } - } - return (crossings & 1) == 1; -} - -float distanceToEdge(vec2 p) { - float minDist = 1000.0; - - for (int i = 0, j = pointCount - 1; i < pointCount; j = i++) { - vec2 edge0 = points[j]; - vec2 edge1 = points[i]; - - if (distance(edge0, edge1) < 0.0001) continue; - - vec2 v1 = p - edge0; - vec2 v2 = edge1 - edge0; - float l2 = dot(v2, v2); - - if (l2 < 0.0001) { - float dist = length(v1); - minDist = min(minDist, dist); - continue; - } - - float t = clamp(dot(v1, v2) / l2, 0.0, 1.0); - vec2 projection = edge0 + t * v2; - float dist = length(p - projection); - minDist = min(minDist, dist); - } - - return minDist; -} - void main() { - vec2 pixel = fragTexCoord * resolution; - - // Compute pixel size for anti-aliasing - vec2 pixelGrad = vec2(dFdx(pixel.x), dFdy(pixel.y)); - float pixelSize = length(pixelGrad); - float aaWidth = max(0.5, pixelSize * 1.5); - - bool inside = isPointInsidePolygon(pixel); - if (inside) { - finalColor = useGradient == 1 ? getGradientColor(pixel) : fillColor; - return; - } - - float sd = -distanceToEdge(pixel); - float alpha = smoothstep(-aaWidth, aaWidth, sd); - if (alpha > 0.0){ - vec4 color = useGradient == 1 ? getGradientColor(pixel) : fillColor; - finalColor = vec4(color.rgb, color.a * alpha); - } else { - discard; - } + // TODO: do proper antialiasing + finalColor = useGradient == 1 ? getGradientColor(gl_FragCoord.xy) : fillColor; } """ @@ -155,14 +116,10 @@ class ShaderState: self.initialized = False self.shader = None - self.white_texture = None # Shader uniform locations self.locations = { - 'pointCount': None, 'fillColor': None, - 'resolution': None, - 'points': None, 'useGradient': None, 'gradientStart': None, 'gradientEnd': None, @@ -173,12 +130,8 @@ class ShaderState: } # Pre-allocated FFI objects - self.point_count_ptr = rl.ffi.new("int[]", [0]) - self.resolution_ptr = rl.ffi.new("float[]", [0.0, 0.0]) self.fill_color_ptr = rl.ffi.new("float[]", [0.0, 0.0, 0.0, 0.0]) self.use_gradient_ptr = rl.ffi.new("int[]", [0]) - self.gradient_start_ptr = rl.ffi.new("float[]", [0.0, 0.0]) - self.gradient_end_ptr = rl.ffi.new("float[]", [0.0, 0.0]) self.color_count_ptr = rl.ffi.new("int[]", [0]) self.gradient_colors_ptr = rl.ffi.new("float[]", MAX_GRADIENT_COLORS * 4) self.gradient_stops_ptr = rl.ffi.new("float[]", MAX_GRADIENT_COLORS) @@ -189,30 +142,19 @@ class ShaderState: self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAGMENT_SHADER) - # Create and cache white texture - white_img = rl.gen_image_color(2, 2, rl.WHITE) - self.white_texture = rl.load_texture_from_image(white_img) - rl.set_texture_filter(self.white_texture, rl.TEXTURE_FILTER_BILINEAR) - rl.unload_image(white_img) - # Cache all uniform locations for uniform in self.locations.keys(): self.locations[uniform] = rl.get_shader_location(self.shader, uniform) - # Setup default MVP matrix - mvp_ptr = rl.ffi.new("float[16]", [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]) - rl.set_shader_value_matrix(self.shader, self.locations['mvp'], rl.Matrix(*mvp_ptr)) + # Orthographic MVP (origin top-left) + proj = rl.matrix_ortho(0, gui_app.width, gui_app.height, 0, -1, 1) + rl.set_shader_value_matrix(self.shader, self.locations['mvp'], proj) self.initialized = True def cleanup(self): if not self.initialized: return - - if self.white_texture: - rl.unload_texture(self.white_texture) - self.white_texture = None - if self.shader: rl.unload_shader(self.shader) self.shader = None @@ -220,103 +162,82 @@ class ShaderState: self.initialized = False -def _configure_shader_color(state, color, gradient, clipped_rect, original_rect): - use_gradient = 1 if gradient else 0 +def _configure_shader_color(state: ShaderState, color: Optional[rl.Color], # noqa: UP045 + gradient: Gradient | None, origin_rect: rl.Rectangle): + assert (color is not None) != (gradient is not None), "Either color or gradient must be provided" + + use_gradient = 1 if (gradient is not None and len(gradient.colors) >= 1) else 0 state.use_gradient_ptr[0] = use_gradient rl.set_shader_value(state.shader, state.locations['useGradient'], state.use_gradient_ptr, UNIFORM_INT) if use_gradient: - start = np.array(gradient['start']) * np.array([original_rect.width, original_rect.height]) + np.array([original_rect.x, original_rect.y]) - end = np.array(gradient['end']) * np.array([original_rect.width, original_rect.height]) + np.array([original_rect.x, original_rect.y]) - start = start - np.array([clipped_rect.x, clipped_rect.y]) - end = end - np.array([clipped_rect.x, clipped_rect.y]) - state.gradient_start_ptr[0:2] = start.astype(np.float32) - state.gradient_end_ptr[0:2] = end.astype(np.float32) - rl.set_shader_value(state.shader, state.locations['gradientStart'], state.gradient_start_ptr, UNIFORM_VEC2) - rl.set_shader_value(state.shader, state.locations['gradientEnd'], state.gradient_end_ptr, UNIFORM_VEC2) + gradient = cast(Gradient, gradient) + state.color_count_ptr[0] = len(gradient.colors) + for i in range(len(gradient.colors)): + c = gradient.colors[i] + base = i * 4 + state.gradient_colors_ptr[base:base + 4] = [c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0] + rl.set_shader_value_v(state.shader, state.locations['gradientColors'], state.gradient_colors_ptr, UNIFORM_VEC4, len(gradient.colors)) - colors = gradient['colors'] - color_count = min(len(colors), MAX_GRADIENT_COLORS) - state.color_count_ptr[0] = color_count - for i, c in enumerate(colors[:color_count]): - base_idx = i * 4 - state.gradient_colors_ptr[base_idx:base_idx+4] = [c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0] - rl.set_shader_value_v(state.shader, state.locations['gradientColors'], state.gradient_colors_ptr, UNIFORM_VEC4, color_count) - - stops = gradient.get('stops', [i / max(1, color_count - 1) for i in range(color_count)]) - stops = np.clip(stops[:color_count], 0.0, 1.0) - state.gradient_stops_ptr[0:color_count] = stops - rl.set_shader_value_v(state.shader, state.locations['gradientStops'], state.gradient_stops_ptr, UNIFORM_FLOAT, color_count) + for i in range(len(gradient.stops)): + s = float(gradient.stops[i]) + state.gradient_stops_ptr[i] = 0.0 if s < 0.0 else 1.0 if s > 1.0 else s + rl.set_shader_value_v(state.shader, state.locations['gradientStops'], state.gradient_stops_ptr, UNIFORM_FLOAT, len(gradient.stops)) rl.set_shader_value(state.shader, state.locations['gradientColorCount'], state.color_count_ptr, UNIFORM_INT) + + # Map normalized start/end to screen pixels + start_vec = rl.Vector2(origin_rect.x + gradient.start[0] * origin_rect.width, origin_rect.y + gradient.start[1] * origin_rect.height) + end_vec = rl.Vector2(origin_rect.x + gradient.end[0] * origin_rect.width, origin_rect.y + gradient.end[1] * origin_rect.height) + rl.set_shader_value(state.shader, state.locations['gradientStart'], start_vec, UNIFORM_VEC2) + rl.set_shader_value(state.shader, state.locations['gradientEnd'], end_vec, UNIFORM_VEC2) else: color = color or rl.WHITE state.fill_color_ptr[0:4] = [color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a / 255.0] rl.set_shader_value(state.shader, state.locations['fillColor'], state.fill_color_ptr, UNIFORM_VEC4) -def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, color=None, gradient=None): - """ - Draw a complex polygon using shader-based even-odd fill rule +def triangulate(pts: np.ndarray) -> list[tuple[float, float]]: + """Only supports simple polygons with two chains (ribbon).""" - Args: - rect: Rectangle defining the drawing area - points: numpy array of (x,y) points defining the polygon - color: Solid fill color (rl.Color) - gradient: Dict with gradient parameters: - { - 'start': (x1, y1), # Start point (normalized 0-1) - 'end': (x2, y2), # End point (normalized 0-1) - 'colors': [rl.Color], # List of colors at stops - 'stops': [float] # List of positions (0-1) - } + # TODO: consider deduping close screenspace points + # interleave points to produce a triangle strip + assert len(pts) % 2 == 0, "Interleaving expects even number of points" + + tri_strip = [] + for i in range(len(pts) // 2): + tri_strip.append(pts[i]) + tri_strip.append(pts[-i - 1]) + + return cast(list, np.array(tri_strip).tolist()) + + +def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, + color: Optional[rl.Color] = None, gradient: Gradient | None = None): # noqa: UP045 + + """ + Draw a ribbon polygon (two chains) with a triangle strip and gradient. + - Input must be [L0..Lk-1, Rk-1..R0], even count, no crossings/holes. """ if len(points) < 3: return + # Initialize shader on-demand state = ShaderState.get_instance() - if not state.initialized: - state.initialize() + state.initialize() - # Find bounding box - min_xy = np.min(points, axis=0) - max_xy = np.max(points, axis=0) - clip_x = max(origin_rect.x, min_xy[0]) - clip_y = max(origin_rect.y, min_xy[1]) - clip_right = min(origin_rect.x + origin_rect.width, max_xy[0]) - clip_bottom = min(origin_rect.y + origin_rect.height, max_xy[1]) + # Ensure (N,2) float32 contiguous array + pts = np.ascontiguousarray(points, dtype=np.float32) + assert pts.ndim == 2 and pts.shape[1] == 2, "points must be (N,2)" - # Check if polygon is completely off-screen - if clip_x >= clip_right or clip_y >= clip_bottom: - return + # Configure gradient shader + _configure_shader_color(state, color, gradient, origin_rect) - clipped_rect = rl.Rectangle(clip_x, clip_y, clip_right - clip_x, clip_bottom - clip_y) + # Triangulate via interleaving + tri_strip = triangulate(pts) - # Transform points relative to the CLIPPED area - transformed_points = points - np.array([clip_x, clip_y]) - - # Set shader values - state.point_count_ptr[0] = len(transformed_points) - rl.set_shader_value(state.shader, state.locations['pointCount'], state.point_count_ptr, UNIFORM_INT) - - state.resolution_ptr[0:2] = [clipped_rect.width, clipped_rect.height] - rl.set_shader_value(state.shader, state.locations['resolution'], state.resolution_ptr, UNIFORM_VEC2) - - flat_points = np.ascontiguousarray(transformed_points.flatten().astype(np.float32)) - points_ptr = rl.ffi.cast("float *", flat_points.ctypes.data) - rl.set_shader_value_v(state.shader, state.locations['points'], points_ptr, UNIFORM_VEC2, len(transformed_points)) - - _configure_shader_color(state, color, gradient, clipped_rect, origin_rect) - - # Render + # Draw strip, color here doesn't matter rl.begin_shader_mode(state.shader) - rl.draw_texture_pro( - state.white_texture, - rl.Rectangle(0, 0, 2, 2), - clipped_rect, - rl.Vector2(0, 0), - 0.0, - rl.WHITE, - ) + rl.draw_triangle_strip(tri_strip, len(tri_strip), rl.WHITE) rl.end_shader_mode() diff --git a/system/ui/lib/text_measure.py b/system/ui/lib/text_measure.py index c172f94251..544ba5b870 100644 --- a/system/ui/lib/text_measure.py +++ b/system/ui/lib/text_measure.py @@ -1,14 +1,35 @@ import pyray as rl +from openpilot.system.ui.lib.application import FONT_SCALE, font_fallback +from openpilot.system.ui.lib.emoji import find_emoji _cache: dict[int, rl.Vector2] = {} def measure_text_cached(font: rl.Font, text: str, font_size: int, spacing: int = 0) -> rl.Vector2: """Caches text measurements to avoid redundant calculations.""" + font = font_fallback(font) key = hash((font.texture.id, text, font_size, spacing)) if key in _cache: return _cache[key] - result = rl.measure_text_ex(font, text, font_size, spacing) # noqa: TID251 + # Measure normal characters without emojis, then add standard width for each found emoji + emoji = find_emoji(text) + if emoji: + non_emoji_text = "" + last_index = 0 + for start, end, _ in emoji: + non_emoji_text += text[last_index:start] + last_index = end + non_emoji_text += text[last_index:] + else: + non_emoji_text = text + + result = rl.measure_text_ex(font, non_emoji_text, font_size * FONT_SCALE, spacing) # noqa: TID251 + if emoji: + result.x += len(emoji) * font_size * FONT_SCALE + # If just emoji assume a single line height + if result.y == 0: + result.y = font_size * FONT_SCALE + _cache[key] = result return result diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index af9ae943ea..4cf2ccebc8 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -2,6 +2,7 @@ import atexit import threading import time import uuid +import subprocess from collections.abc import Callable from dataclasses import dataclass from enum import IntEnum @@ -22,9 +23,14 @@ from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_80 NM_802_11_AP_FLAGS_PRIVACY, NM_802_11_AP_FLAGS_WPS, NM_PATH, NM_IFACE, NM_ACCESS_POINT_IFACE, NM_SETTINGS_PATH, NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE, - NM_DEVICE_TYPE_WIFI, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, - NM_DEVICE_STATE_REASON_NEW_ACTIVATION, - NMDeviceState) + NM_DEVICE_TYPE_WIFI, NM_DEVICE_TYPE_MODEM, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, + NM_DEVICE_STATE_REASON_NEW_ACTIVATION, NM_ACTIVE_CONNECTION_IFACE, + NM_IP4_CONFIG_IFACE, NMDeviceState) + +try: + from openpilot.common.params import Params +except Exception: + Params = None TETHERING_IP_ADDRESS = "192.168.43.1" DEFAULT_TETHERING_PASSWORD = "swagswagcomma" @@ -40,6 +46,12 @@ class SecurityType(IntEnum): UNSUPPORTED = 4 +class MeteredType(IntEnum): + UNKNOWN = 0 + YES = 1 + NO = 2 + + def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType: wpa_props = wpa_flags | rsn_flags @@ -114,7 +126,7 @@ class AccessPoint: class WifiManager: def __init__(self): - self._networks = [] # a network can be comprised of multiple APs + self._networks: list[Network] = [] # a network can be comprised of multiple APs self._active = True # used to not run when not in settings self._exit = False @@ -132,39 +144,79 @@ class WifiManager: # State self._connecting_to_ssid: str = "" + self._ipv4_address: str = "" + self._current_network_metered: MeteredType = MeteredType.UNKNOWN + self._tethering_password: str = "" + self._ipv4_forward = False + self._last_network_update: float = 0.0 self._callback_queue: list[Callable] = [] + self._tethering_ssid = "weedle" + if Params is not None: + dongle_id = Params().get("DongleId") + if dongle_id: + self._tethering_ssid += "-" + dongle_id[:4] + # Callbacks - self._need_auth: Callable[[str], None] | None = None - self._activated: Callable[[], None] | None = None - self._forgotten: Callable[[], None] | None = None - self._networks_updated: Callable[[list[Network]], None] | None = None - self._disconnected: Callable[[], None] | None = None + self._need_auth: list[Callable[[str], None]] = [] + self._activated: list[Callable[[], None]] = [] + self._forgotten: list[Callable[[], None]] = [] + self._networks_updated: list[Callable[[list[Network]], None]] = [] + self._disconnected: list[Callable[[], None]] = [] self._lock = threading.Lock() - self._scan_thread = threading.Thread(target=self._network_scanner, daemon=True) - self._scan_thread.start() - self._state_thread = threading.Thread(target=self._monitor_state, daemon=True) - self._state_thread.start() - + self._initialize() atexit.register(self.stop) - def set_callbacks(self, need_auth: Callable[[str], None], - activated: Callable[[], None] | None, - forgotten: Callable[[], None], - networks_updated: Callable[[list[Network]], None], - disconnected: Callable[[], None]): - self._need_auth = need_auth - self._activated = activated - self._forgotten = forgotten - self._networks_updated = networks_updated - self._disconnected = disconnected + def _initialize(self): + def worker(): + self._wait_for_wifi_device() - def _enqueue_callback(self, cb: Callable, *args): - self._callback_queue.append(lambda: cb(*args)) + self._scan_thread.start() + self._state_thread.start() + + if Params is not None and self._tethering_ssid not in self._get_connections(): + self._add_tethering_connection() + + self._tethering_password = self._get_tethering_password() + cloudlog.debug("WifiManager initialized") + + threading.Thread(target=worker, daemon=True).start() + + def add_callbacks(self, need_auth: Callable[[str], None] | None = None, + activated: Callable[[], None] | None = None, + forgotten: Callable[[], None] | None = None, + networks_updated: Callable[[list[Network]], None] | None = None, + disconnected: Callable[[], None] | None = None): + if need_auth is not None: + self._need_auth.append(need_auth) + if activated is not None: + self._activated.append(activated) + if forgotten is not None: + self._forgotten.append(forgotten) + if networks_updated is not None: + self._networks_updated.append(networks_updated) + if disconnected is not None: + self._disconnected.append(disconnected) + + @property + def ipv4_address(self) -> str: + return self._ipv4_address + + @property + def current_network_metered(self) -> MeteredType: + return self._current_network_metered + + @property + def tethering_password(self) -> str: + return self._tethering_password + + def _enqueue_callbacks(self, cbs: list[Callable], *args): + for cb in cbs: + self._callback_queue.append(lambda _cb=cb: _cb(*args)) def process_callbacks(self): # Call from UI thread to run any pending callbacks @@ -180,15 +232,11 @@ class WifiManager: self._last_network_update = 0.0 def _monitor_state(self): - device_path = self._wait_for_wifi_device() - if device_path is None: - return - rule = MatchRule( type="signal", interface=NM_DEVICE_IFACE, member="StateChanged", - path=device_path, + path=self._wifi_device, ) # Filter for StateChanged signal @@ -211,24 +259,20 @@ class WifiManager: # BAD PASSWORD if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): self.forget_connection(self._connecting_to_ssid, block=True) - if self._need_auth is not None: - self._enqueue_callback(self._need_auth, self._connecting_to_ssid) + self._enqueue_callbacks(self._need_auth, self._connecting_to_ssid) self._connecting_to_ssid = "" elif new_state == NMDeviceState.ACTIVATED: - if self._activated is not None: + if len(self._activated): self._update_networks() - self._enqueue_callback(self._activated) + self._enqueue_callbacks(self._activated) self._connecting_to_ssid = "" elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: self._connecting_to_ssid = "" - if self._disconnected is not None: - self._enqueue_callback(self._disconnected) + self._enqueue_callbacks(self._forgotten) def _network_scanner(self): - self._wait_for_wifi_device() - while not self._exit: if self._active: if time.monotonic() - self._last_network_update > SCAN_PERIOD_SECONDS: @@ -239,30 +283,26 @@ class WifiManager: self._last_network_update = time.monotonic() time.sleep(1 / 2.) - def _wait_for_wifi_device(self) -> str | None: - with self._lock: - device_path: str | None = None - while not self._exit: - device_path = self._get_wifi_device() - if device_path is not None: - break - time.sleep(1) - return device_path - - def _get_wifi_device(self) -> str | None: - if self._wifi_device is not None: - return self._wifi_device - - device_paths = self._router_main.send_and_get_reply(new_method_call(self._nm, 'GetDevices')).body[0] - for device_path in device_paths: - dev_addr = DBusAddress(device_path, bus_name=NM, interface=NM_DEVICE_IFACE) - dev_type = self._router_main.send_and_get_reply(Properties(dev_addr).get('DeviceType')).body[0][1] - - if dev_type == NM_DEVICE_TYPE_WIFI: + def _wait_for_wifi_device(self): + while not self._exit: + device_path = self._get_adapter(NM_DEVICE_TYPE_WIFI) + if device_path is not None: self._wifi_device = device_path break + time.sleep(1) - return self._wifi_device + def _get_adapter(self, adapter_type: int) -> str | None: + # Return the first NetworkManager device path matching adapter_type + try: + device_paths = self._router_main.send_and_get_reply(new_method_call(self._nm, 'GetDevices')).body[0] + for device_path in device_paths: + dev_addr = DBusAddress(device_path, bus_name=NM, interface=NM_DEVICE_IFACE) + dev_type = self._router_main.send_and_get_reply(Properties(dev_addr).get('DeviceType')).body[0][1] + if dev_type == adapter_type: + return str(device_path) + except Exception as e: + cloudlog.exception(f"Error getting adapter type {adapter_type}: {e}") + return None def _get_connections(self) -> dict[str, str]: settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) @@ -270,29 +310,72 @@ class WifiManager: conns: dict[str, str] = {} for conn_path in known_connections: - conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) - reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, "GetSettings")) + settings = self._get_connection_settings(conn_path) - # ignore connections removed during iteration (need auth, etc.) - if reply.header.message_type == MessageType.error: - cloudlog.warning(f"Failed to get connection properties for {conn_path}") + if len(settings) == 0: + cloudlog.warning(f'Failed to get connection settings for {conn_path}') continue - settings = reply.body[0] if "802-11-wireless" in settings: ssid = settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") if ssid != "": conns[ssid] = conn_path return conns - def connect_to_network(self, ssid: str, password: str): + def _get_active_connections(self): + return self._router_main.send_and_get_reply(Properties(self._nm).get('ActiveConnections')).body[0][1] + + def _get_connection_settings(self, conn_path: str) -> dict: + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'GetSettings')) + if reply.header.message_type == MessageType.error: + cloudlog.warning(f'Failed to get connection settings: {reply}') + return {} + return dict(reply.body[0]) + + def _add_tethering_connection(self): + connection = { + 'connection': { + 'type': ('s', '802-11-wireless'), + 'uuid': ('s', str(uuid.uuid4())), + 'id': ('s', 'Hotspot'), + 'autoconnect-retries': ('i', 0), + 'interface-name': ('s', 'wlan0'), + 'autoconnect': ('b', False), + }, + '802-11-wireless': { + 'band': ('s', 'bg'), + 'mode': ('s', 'ap'), + 'ssid': ('ay', self._tethering_ssid.encode("utf-8")), + }, + '802-11-wireless-security': { + 'group': ('as', ['ccmp']), + 'key-mgmt': ('s', 'wpa-psk'), + 'pairwise': ('as', ['ccmp']), + 'proto': ('as', ['rsn']), + 'psk': ('s', DEFAULT_TETHERING_PASSWORD), + }, + 'ipv4': { + 'method': ('s', 'shared'), + 'address-data': ('aa{sv}', [[ + ('address', ('s', TETHERING_IP_ADDRESS)), + ('prefix', ('u', 24)), + ]]), + 'gateway': ('s', TETHERING_IP_ADDRESS), + 'never-default': ('b', True), + }, + 'ipv6': {'method': ('s', 'ignore')}, + } + + settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) + self._router_main.send_and_get_reply(new_method_call(settings_addr, 'AddConnection', 'a{sa{sv}}', (connection,))) + + def connect_to_network(self, ssid: str, password: str, hidden: bool = False): def worker(): # Clear all connections that may already exist to the network we are connecting to self._connecting_to_ssid = ssid self.forget_connection(ssid, block=True) - is_hidden = False - connection = { 'connection': { 'type': ('s', '802-11-wireless'), @@ -302,7 +385,7 @@ class WifiManager: }, '802-11-wireless': { 'ssid': ('ay', ssid.encode("utf-8")), - 'hidden': ('b', is_hidden), + 'hidden': ('b', hidden), 'mode': ('s', 'infrastructure'), }, 'ipv4': { @@ -332,9 +415,9 @@ class WifiManager: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) - if self._forgotten is not None: + if len(self._forgotten): self._update_networks() - self._enqueue_callback(self._forgotten) + self._enqueue_callbacks(self._forgotten) if block: worker() @@ -358,6 +441,144 @@ class WifiManager: else: threading.Thread(target=worker, daemon=True).start() + def _deactivate_connection(self, ssid: str): + for conn_path in self._get_active_connections(): + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) + specific_obj_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('SpecificObject')).body[0][1] + + if specific_obj_path != "/": + ap_addr = DBusAddress(specific_obj_path, bus_name=NM, interface=NM_ACCESS_POINT_IFACE) + ap_ssid = bytes(self._router_main.send_and_get_reply(Properties(ap_addr).get('Ssid')).body[0][1]).decode("utf-8", "replace") + + if ap_ssid == ssid: + self._router_main.send_and_get_reply(new_method_call(self._nm, 'DeactivateConnection', 'o', (conn_path,))) + return + + def is_tethering_active(self) -> bool: + for network in self._networks: + if network.is_connected: + return bool(network.ssid == self._tethering_ssid) + return False + + def set_tethering_password(self, password: str): + def worker(): + conn_path = self._get_connections().get(self._tethering_ssid, None) + if conn_path is None: + cloudlog.warning('No tethering connection found') + return + + settings = self._get_connection_settings(conn_path) + if len(settings) == 0: + cloudlog.warning(f'Failed to get tethering settings for {conn_path}') + return + + settings['802-11-wireless-security']['psk'] = ('s', password) + + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Update', 'a{sa{sv}}', (settings,))) + if reply.header.message_type == MessageType.error: + cloudlog.warning(f'Failed to update tethering settings: {reply}') + return + + self._tethering_password = password + if self.is_tethering_active(): + self.activate_connection(self._tethering_ssid, block=True) + + threading.Thread(target=worker, daemon=True).start() + + def _get_tethering_password(self) -> str: + conn_path = self._get_connections().get(self._tethering_ssid, None) + if conn_path is None: + cloudlog.warning('No tethering connection found') + return '' + + reply = self._router_main.send_and_get_reply(new_method_call( + DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE), + 'GetSecrets', 's', ('802-11-wireless-security',) + )) + + if reply.header.message_type == MessageType.error: + cloudlog.warning(f'Failed to get tethering password: {reply}') + return '' + + secrets = reply.body[0] + if '802-11-wireless-security' not in secrets: + return '' + + return str(secrets['802-11-wireless-security'].get('psk', ('s', ''))[1]) + + def set_ipv4_forward(self, enabled: bool): + self._ipv4_forward = enabled + + def set_tethering_active(self, active: bool): + def worker(): + if active: + self.activate_connection(self._tethering_ssid, block=True) + + if not self._ipv4_forward: + time.sleep(5) + cloudlog.warning("net.ipv4.ip_forward = 0") + subprocess.run(["sudo", "sysctl", "net.ipv4.ip_forward=0"], check=False) + else: + self._deactivate_connection(self._tethering_ssid) + + threading.Thread(target=worker, daemon=True).start() + + def _update_current_network_metered(self) -> None: + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return + + self._current_network_metered = MeteredType.UNKNOWN + for active_conn in self._get_active_connections(): + conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) + conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] + + if conn_type == '802-11-wireless': + conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1] + if conn_path == "/": + continue + + settings = self._get_connection_settings(conn_path) + + if len(settings) == 0: + cloudlog.warning(f'Failed to get connection settings for {conn_path}') + continue + + metered_prop = settings['connection'].get('metered', ('i', 0))[1] + if metered_prop == MeteredType.YES: + self._current_network_metered = MeteredType.YES + elif metered_prop == MeteredType.NO: + self._current_network_metered = MeteredType.NO + return + + def set_current_network_metered(self, metered: MeteredType): + def worker(): + for active_conn in self._get_active_connections(): + conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) + conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] + + if conn_type == '802-11-wireless' and not self.is_tethering_active(): + conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1] + if conn_path == "/": + continue + + settings = self._get_connection_settings(conn_path) + + if len(settings) == 0: + cloudlog.warning(f'Failed to get connection settings for {conn_path}') + return + + settings['connection']['metered'] = ('i', int(metered)) + + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Update', 'a{sa{sv}}', (settings,))) + if reply.header.message_type == MessageType.error: + cloudlog.warning(f'Failed to update tethering settings: {reply}') + return + + threading.Thread(target=worker, daemon=True).start() + def _request_scan(self): if self._wifi_device is None: cloudlog.warning("No WiFi device found") @@ -409,17 +630,126 @@ class WifiManager: networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower())) self._networks = networks - if self._networks_updated is not None: - self._enqueue_callback(self._networks_updated, self._networks) + self._update_ipv4_address() + self._update_current_network_metered() + + self._enqueue_callbacks(self._networks_updated, self._networks) + + def _update_ipv4_address(self): + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return + + self._ipv4_address = "" + + for conn_path in self._get_active_connections(): + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE) + conn_type = self._router_main.send_and_get_reply(Properties(conn_addr).get('Type')).body[0][1] + if conn_type == '802-11-wireless': + ip4config_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Ip4Config')).body[0][1] + + if ip4config_path != "/": + ip4config_addr = DBusAddress(ip4config_path, bus_name=NM, interface=NM_IP4_CONFIG_IFACE) + address_data = self._router_main.send_and_get_reply(Properties(ip4config_addr).get('AddressData')).body[0][1] + + for entry in address_data: + if 'address' in entry: + self._ipv4_address = entry['address'][1] + return def __del__(self): self.stop() + def update_gsm_settings(self, roaming: bool, apn: str, metered: bool): + """Update GSM settings for cellular connection""" + + def worker(): + try: + lte_connection_path = self._get_lte_connection_path() + if not lte_connection_path: + cloudlog.warning("No LTE connection found") + return + + settings = self._get_connection_settings(lte_connection_path) + + if len(settings) == 0: + cloudlog.warning(f"Failed to get connection settings for {lte_connection_path}") + return + + # Ensure dicts exist + if 'gsm' not in settings: + settings['gsm'] = {} + if 'connection' not in settings: + settings['connection'] = {} + + changes = False + auto_config = apn == "" + + if settings['gsm'].get('auto-config', ('b', False))[1] != auto_config: + cloudlog.warning(f'Changing gsm.auto-config to {auto_config}') + settings['gsm']['auto-config'] = ('b', auto_config) + changes = True + + if settings['gsm'].get('apn', ('s', ''))[1] != apn: + cloudlog.warning(f'Changing gsm.apn to {apn}') + settings['gsm']['apn'] = ('s', apn) + changes = True + + if settings['gsm'].get('home-only', ('b', False))[1] == roaming: + cloudlog.warning(f'Changing gsm.home-only to {not roaming}') + settings['gsm']['home-only'] = ('b', not roaming) + changes = True + + # Unknown means NetworkManager decides + metered_int = int(MeteredType.UNKNOWN if metered else MeteredType.NO) + if settings['connection'].get('metered', ('i', 0))[1] != metered_int: + cloudlog.warning(f'Changing connection.metered to {metered_int}') + settings['connection']['metered'] = ('i', metered_int) + changes = True + + if changes: + # Update the connection settings (temporary update) + conn_addr = DBusAddress(lte_connection_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, 'UpdateUnsaved', 'a{sa{sv}}', (settings,))) + + if reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to update GSM settings: {reply}") + return + + self._activate_modem_connection(lte_connection_path) + except Exception as e: + cloudlog.exception(f"Error updating GSM settings: {e}") + + threading.Thread(target=worker, daemon=True).start() + + def _get_lte_connection_path(self) -> str | None: + try: + settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) + known_connections = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0] + + for conn_path in known_connections: + settings = self._get_connection_settings(conn_path) + if settings and settings.get('connection', {}).get('id', ('s', ''))[1] == 'lte': + return str(conn_path) + except Exception as e: + cloudlog.exception(f"Error finding LTE connection: {e}") + return None + + def _activate_modem_connection(self, connection_path: str): + try: + modem_device = self._get_adapter(NM_DEVICE_TYPE_MODEM) + if modem_device and connection_path: + self._router_main.send_and_get_reply(new_method_call(self._nm, 'ActivateConnection', 'ooo', (connection_path, modem_device, "/"))) + except Exception as e: + cloudlog.exception(f"Error activating modem connection: {e}") + def stop(self): if not self._exit: self._exit = True - self._scan_thread.join() - self._state_thread.join() + if self._scan_thread.is_alive(): + self._scan_thread.join() + if self._state_thread.is_alive(): + self._state_thread.join() self._router_main.close() self._router_main.conn.close() diff --git a/system/ui/lib/wrap_text.py b/system/ui/lib/wrap_text.py index 35dc5ac401..745d37b468 100644 --- a/system/ui/lib/wrap_text.py +++ b/system/ui/lib/wrap_text.py @@ -1,5 +1,6 @@ import pyray as rl from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.application import font_fallback def _break_long_word(font: rl.Font, word: str, font_size: int, max_width: int) -> list[str]: @@ -36,7 +37,15 @@ def _break_long_word(font: rl.Font, word: str, font_size: int, max_width: int) - return parts +_cache: dict[int, list[str]] = {} + + def wrap_text(font: rl.Font, text: str, font_size: int, max_width: int) -> list[str]: + font = font_fallback(font) + key = hash((font.texture.id, text, font_size, max_width)) + if key in _cache: + return _cache[key] + if not text or max_width <= 0: return [] @@ -58,8 +67,6 @@ def wrap_text(font: rl.Font, text: str, font_size: int, max_width: int) -> list[ lines: list[str] = [] current_line: list[str] = [] - current_width = 0 - space_width = int(measure_text_cached(font, " ", font_size).x) for word in words: word_width = int(measure_text_cached(font, word, font_size).x) @@ -70,28 +77,23 @@ def wrap_text(font: rl.Font, text: str, font_size: int, max_width: int) -> list[ if current_line: lines.append(" ".join(current_line)) current_line = [] - current_width = 0 # Break the long word into parts lines.extend(_break_long_word(font, word, font_size, max_width)) continue - # Calculate width if we add this word - needed_width = current_width - if current_line: # Need space before word - needed_width += space_width - needed_width += word_width + # Measure the actual joined string to get accurate width (accounts for kerning, etc.) + test_line = " ".join(current_line + [word]) if current_line else word + test_width = int(measure_text_cached(font, test_line, font_size).x) # Check if word fits on current line - if needed_width <= max_width: + if test_width <= max_width: current_line.append(word) - current_width = needed_width else: # Start new line with this word if current_line: lines.append(" ".join(current_line)) current_line = [word] - current_width = word_width # Add remaining words if current_line: @@ -100,4 +102,5 @@ def wrap_text(font: rl.Font, text: str, font_size: int, max_width: int) -> list[ # Add all lines from this paragraph all_lines.extend(lines) + _cache[key] = all_lines return all_lines diff --git a/system/ui/reset.py b/system/ui/reset.py index a5cf1731dc..3922c27aac 100755 --- a/system/ui/reset.py +++ b/system/ui/reset.py @@ -8,7 +8,7 @@ from enum import IntEnum import pyray as rl from openpilot.system.hardware import PC -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.label import gui_label, gui_text_box @@ -70,10 +70,10 @@ class Reset(Widget): exit(0) def _render(self, rect: rl.Rectangle): - label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100) + label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100 * FONT_SCALE) gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) - text_rect = rl.Rectangle(rect.x + 140, rect.y + 140, rect.width - 280, rect.height - 90 - 100) + text_rect = rl.Rectangle(rect.x + 140, rect.y + 140, rect.width - 280, rect.height - 90 - 100 * FONT_SCALE) gui_text_box(text_rect, self._get_body_text(), 90) button_height = 160 @@ -126,9 +126,10 @@ def main(): if mode == ResetMode.FORMAT: reset.start_reset() - for _ in gui_app.render(): - if not reset.render(rl.Rectangle(45, 200, gui_app.width - 90, gui_app.height - 245)): - break + for should_render in gui_app.render(): + if should_render: + if not reset.render(rl.Rectangle(45, 200, gui_app.width - 90, gui_app.height - 245)): + break if __name__ == "__main__": diff --git a/system/ui/setup.py b/system/ui/setup.py index a985e783be..0045b45417 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -4,6 +4,7 @@ import re import threading import time import urllib.request +import urllib.error from urllib.parse import urlparse from enum import IntEnum import shutil @@ -11,23 +12,23 @@ import shutil import pyray as rl from cereal import log -from openpilot.common.run import run_cmd +from openpilot.common.utils import run_cmd from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.keyboard import Keyboard -from openpilot.system.ui.widgets.label import Label, TextAlignment +from openpilot.system.ui.widgets.label import Label from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManager NetworkType = log.DeviceState.NetworkType MARGIN = 50 -TITLE_FONT_SIZE = 116 +TITLE_FONT_SIZE = 90 TITLE_FONT_WEIGHT = FontWeight.MEDIUM NEXT_BUTTON_WIDTH = 310 -BODY_FONT_SIZE = 96 +BODY_FONT_SIZE = 80 BUTTON_HEIGHT = 160 BUTTON_SPACING = 50 @@ -48,6 +49,7 @@ cd /data/openpilot exec ./launch_openpilot.sh """ + class SetupState(IntEnum): LOW_VOLTAGE = 0 GETTING_STARTED = 1 @@ -78,16 +80,17 @@ class Setup(Widget): self.warning = gui_app.texture("icons/warning.png", 150, 150) self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) - self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255)) + self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + text_color=rl.Color(255, 89, 79, 255), text_padding=20) self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE, - text_alignment=TextAlignment.LEFT) + text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback) self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown) self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0) - self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.", - BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) @@ -95,36 +98,38 @@ class Setup(Widget): button_style=ButtonStyle.PRIMARY) self._software_selection_continue_button.set_enabled(False) self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback) - self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + text_padding=20) self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot) self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY) - self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) - self._download_failed_url_label = Label("", 64, FontWeight.NORMAL, TextAlignment.LEFT) - self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback) self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback, button_style=ButtonStyle.PRIMARY) self._network_setup_continue_button.set_enabled(False) - self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback, button_style=ButtonStyle.PRIMARY) self._custom_software_warning_continue_button.set_enabled(False) self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback) - self._custom_software_warning_title_label = Label("WARNING: Custom Software", 100, FontWeight.BOLD, TextAlignment.LEFT, text_color=rl.Color(255,89,79,255), + self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + text_color=rl.Color(255, 89, 79, 255), text_padding=60) self._custom_software_warning_body_label = Label("Use caution when installing third-party software.\n\n" - + "⚠️ It has not been tested by comma.\n\n" - + "⚠️ It may not comply with relevant safety standards.\n\n" - + "⚠️ It may cause damage to your device and/or vehicle.\n\n" - + "If you'd like to proceed, use https://flash.comma.ai " - + "to restore your device to a factory state later.", - 85, text_alignment=TextAlignment.LEFT, text_padding=60) + + "⚠️ It has not been tested by comma.\n\n" + + "⚠️ It may not comply with relevant safety standards.\n\n" + + "⚠️ It may cause damage to your device and/or vehicle.\n\n" + + "If you'd like to proceed, use https://flash.comma.ai " + + "to restore your device to a factory state later.", + 68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=60) self._custom_software_warning_body_scroll_panel = GuiScrollPanel() - self._downloading_body_label = Label("Downloading...", TITLE_FONT_SIZE, FontWeight.MEDIUM) + self._downloading_body_label = Label("Downloading...", TITLE_FONT_SIZE, FontWeight.MEDIUM, text_padding=20) try: with open("/sys/class/hwmon/hwmon1/in1_input") as f: @@ -191,8 +196,8 @@ class Setup(Widget): def render_low_voltage(self, rect: rl.Rectangle): rl.draw_texture(self.warning, int(rect.x + 150), int(rect.y + 110), rl.WHITE) - self._low_voltage_title_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE)) - self._low_voltage_body_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 150, rect.width - 500, BODY_FONT_SIZE * 3)) + self._low_voltage_title_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE * FONT_SCALE)) + self._low_voltage_body_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 150, rect.width - 500, BODY_FONT_SIZE * FONT_SCALE * 3)) button_width = (rect.width - MARGIN * 3) / 2 button_y = rect.height - MARGIN - BUTTON_HEIGHT @@ -200,8 +205,9 @@ class Setup(Widget): self._low_voltage_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) def render_getting_started(self, rect: rl.Rectangle): - self._getting_started_title_label.render(rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE)) - self._getting_started_body_label.render(rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE, rect.width - 500, BODY_FONT_SIZE * 3)) + self._getting_started_title_label.render(rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE * FONT_SCALE)) + self._getting_started_body_label.render(rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE * FONT_SCALE, rect.width - 500, + BODY_FONT_SIZE * FONT_SCALE * 3)) btn_rect = rl.Rectangle(rect.width - NEXT_BUTTON_WIDTH, 0, NEXT_BUTTON_WIDTH, rect.height) self._getting_started_button.render(btn_rect) @@ -233,10 +239,10 @@ class Setup(Widget): self.network_check_thread.join() def render_network_setup(self, rect: rl.Rectangle): - self._network_setup_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) + self._network_setup_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE * FONT_SCALE)) - wifi_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN + 25, rect.width - MARGIN * 2, - rect.height - TITLE_FONT_SIZE - 25 - BUTTON_HEIGHT - MARGIN * 3) + wifi_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE * FONT_SCALE + MARGIN + 25, rect.width - MARGIN * 2, + rect.height - TITLE_FONT_SIZE * FONT_SCALE - 25 - BUTTON_HEIGHT - MARGIN * 3) rl.draw_rectangle_rounded(wifi_rect, 0.05, 10, rl.Color(51, 51, 51, 255)) wifi_content_rect = rl.Rectangle(wifi_rect.x + MARGIN, wifi_rect.y, wifi_rect.width - MARGIN * 2, wifi_rect.height) self.wifi_ui.render(wifi_content_rect) @@ -254,21 +260,22 @@ class Setup(Widget): self._network_setup_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_software_selection(self, rect: rl.Rectangle): - self._software_selection_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) + self._software_selection_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE * FONT_SCALE)) radio_height = 230 radio_spacing = 30 self._software_selection_continue_button.set_enabled(False) - openpilot_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2, rect.width - MARGIN * 2, radio_height) + openpilot_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE * FONT_SCALE + MARGIN * 2, rect.width - MARGIN * 2, radio_height) self._software_selection_openpilot_button.render(openpilot_rect) if self._software_selection_openpilot_button.selected: self._software_selection_continue_button.set_enabled(True) self._software_selection_custom_software_button.selected = False - custom_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2 + radio_height + radio_spacing, rect.width - MARGIN * 2, radio_height) + custom_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE * FONT_SCALE + MARGIN * 2 + radio_height + radio_spacing, rect.width - MARGIN * 2, + radio_height) self._software_selection_custom_software_button.render(custom_rect) if self._software_selection_custom_software_button.selected: @@ -282,12 +289,13 @@ class Setup(Widget): self._software_selection_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_downloading(self, rect: rl.Rectangle): - self._downloading_body_label.render(rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE)) + self._downloading_body_label.render(rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE * FONT_SCALE / 2, rect.width, + TITLE_FONT_SIZE * FONT_SCALE)) def render_download_failed(self, rect: rl.Rectangle): - self._download_failed_title_label.render(rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE)) + self._download_failed_title_label.render(rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE * FONT_SCALE)) self._download_failed_url_label.set_text(self.failed_url) - self._download_failed_url_label.render(rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64)) + self._download_failed_url_label.render(rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE * FONT_SCALE + 67, rect.width - 117 - 100, 64)) self._download_failed_body_label.set_text(self.failed_reason) self._download_failed_body_label.render(rl.Rectangle(rect.x + 117, rect.y, rect.width - 117 - 100, rect.height)) @@ -299,20 +307,20 @@ class Setup(Widget): def render_custom_software_warning(self, rect: rl.Rectangle): warn_rect = rl.Rectangle(rect.x, rect.y, rect.width, 1500) - offset = self._custom_software_warning_body_scroll_panel.handle_scroll(rect, warn_rect) + offset = self._custom_software_warning_body_scroll_panel.update(rect, warn_rect) button_width = (rect.width - MARGIN * 3) / 2 button_y = rect.height - MARGIN - BUTTON_HEIGHT - rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(button_y - BODY_FONT_SIZE)) - y_offset = rect.y + offset.y - self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, y_offset + 150, rect.width - 265, TITLE_FONT_SIZE)) - self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, y_offset + 200 , rect.width - 50, BODY_FONT_SIZE * 3)) + rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(button_y - BODY_FONT_SIZE * FONT_SCALE)) + y_offset = rect.y + offset + self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, y_offset + 150, rect.width - 265, TITLE_FONT_SIZE * FONT_SCALE)) + self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, y_offset + 200, rect.width - 50, BODY_FONT_SIZE * FONT_SCALE * 3)) rl.end_scissor_mode() self._custom_software_warning_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) self._custom_software_warning_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) - if offset.y < (rect.height - warn_rect.height): + if offset < (rect.height - warn_rect.height): self._custom_software_warning_continue_button.set_enabled(True) self._custom_software_warning_continue_button.set_text("Continue") @@ -329,7 +337,7 @@ class Setup(Widget): elif result == 0: self.state = SetupState.SOFTWARE_SELECTION - self.keyboard.reset() + self.keyboard.reset(min_text_size=1) self.keyboard.set_title("Enter URL", "for Custom Software") gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result) @@ -343,7 +351,7 @@ class Setup(Widget): shutil.copyfile(INSTALLER_SOURCE_PATH, INSTALLER_DESTINATION_PATH) # give time for installer UI to take over - time.sleep(1) + time.sleep(0.1) gui_app.request_close() else: self.state = SetupState.NETWORK_SETUP @@ -369,7 +377,9 @@ class Setup(Widget): fd, tmpfile = tempfile.mkstemp(prefix="installer_") - headers = {"User-Agent": USER_AGENT, "X-openpilot-serial": HARDWARE.get_serial()} + headers = {"User-Agent": USER_AGENT, + "X-openpilot-serial": HARDWARE.get_serial(), + "X-openpilot-device-type": HARDWARE.get_device_type()} req = urllib.request.Request(self.download_url, headers=headers) with open(tmpfile, 'wb') as f, urllib.request.urlopen(req, timeout=30) as response: @@ -406,9 +416,13 @@ class Setup(Widget): f.write(self.download_url) # give time for installer UI to take over - time.sleep(5) + time.sleep(0.1) gui_app.request_close() + except urllib.error.HTTPError as e: + if e.code == 409: + error_msg = e.read().decode("utf-8") + self.download_failed(self.download_url, error_msg) except Exception: error_msg = "Ensure the entered URL is valid, and the device's internet connection is good." self.download_failed(self.download_url, error_msg) @@ -423,8 +437,9 @@ def main(): try: gui_app.init_window("Setup", 20) setup = Setup() - for _ in gui_app.render(): - setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + for should_render in gui_app.render(): + if should_render: + setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) setup.close() except Exception as e: print(f"Setup error: {e}") diff --git a/system/ui/sunnypilot/lib/application.py b/system/ui/sunnypilot/lib/application.py index 10227ce9fd..7440d224ca 100644 --- a/system/ui/sunnypilot/lib/application.py +++ b/system/ui/sunnypilot/lib/application.py @@ -3,19 +3,22 @@ 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 __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] + 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: + image_obj = self._load_image_from_path(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + texture_obj = self._load_texture_from_image(image_obj) + self._textures[cache_key] = texture_obj + return texture_obj - 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) diff --git a/system/ui/text.py b/system/ui/text.py index 61ac043b72..707b30983b 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -7,7 +7,7 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.button import Button, ButtonStyle MARGIN = 50 SPACING = 40 @@ -53,27 +53,30 @@ class TextWindow(Widget): self._textarea_rect = rl.Rectangle(MARGIN, MARGIN, gui_app.width - MARGIN * 2, gui_app.height - MARGIN * 2) self._wrapped_lines = wrap_text(text, FONT_SIZE, self._textarea_rect.width - 20) self._content_rect = rl.Rectangle(0, 0, self._textarea_rect.width - 20, len(self._wrapped_lines) * LINE_HEIGHT) - self._scroll_panel = GuiScrollPanel(show_vertical_scroll_bar=True) - self._scroll_panel._offset.y = -max(self._content_rect.height - self._textarea_rect.height, 0) + self._scroll_panel = GuiScrollPanel() + self._scroll_panel._offset_filter_y.x = -max(self._content_rect.height - self._textarea_rect.height, 0) + + button_text = "Exit" if PC else "Reboot" + self._button = Button(button_text, click_callback=self._on_button_clicked, button_style=ButtonStyle.TRANSPARENT_WHITE_BORDER) + + @staticmethod + def _on_button_clicked(): + gui_app.request_close() + if not PC: + HARDWARE.reboot() def _render(self, rect: rl.Rectangle): - scroll = self._scroll_panel.handle_scroll(self._textarea_rect, self._content_rect) + scroll = self._scroll_panel.update(self._textarea_rect, self._content_rect) rl.begin_scissor_mode(int(self._textarea_rect.x), int(self._textarea_rect.y), int(self._textarea_rect.width), int(self._textarea_rect.height)) for i, line in enumerate(self._wrapped_lines): - position = rl.Vector2(self._textarea_rect.x + scroll.x, self._textarea_rect.y + scroll.y + i * LINE_HEIGHT) + position = rl.Vector2(self._textarea_rect.x, self._textarea_rect.y + scroll + i * LINE_HEIGHT) if position.y + LINE_HEIGHT < self._textarea_rect.y or position.y > self._textarea_rect.y + self._textarea_rect.height: continue rl.draw_text_ex(gui_app.font(), line, position, FONT_SIZE, 0, rl.WHITE) rl.end_scissor_mode() button_bounds = rl.Rectangle(rect.width - MARGIN - BUTTON_SIZE.x - SPACING, rect.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y) - ret = gui_button(button_bounds, "Exit" if PC else "Reboot", button_style=ButtonStyle.TRANSPARENT) - if ret: - if PC: - gui_app.request_close() - else: - HARDWARE.reboot() - return ret + self._button.render(button_bounds) if __name__ == "__main__": diff --git a/system/ui/updater.py b/system/ui/updater.py index b3cdc82cf5..2e1a8687e1 100755 --- a/system/ui/updater.py +++ b/system/ui/updater.py @@ -6,10 +6,10 @@ import pyray as rl from enum import IntEnum from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.label import gui_text_box, gui_label from openpilot.system.ui.widgets.network import WifiManagerUI @@ -45,8 +45,17 @@ class Updater(Widget): self.update_thread = None self.wifi_manager_ui = WifiManagerUI(WifiManager()) + # Buttons + self._wifi_button = Button("Connect to Wi-Fi", click_callback=lambda: self.set_current_screen(Screen.WIFI)) + self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY) + self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT)) + self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot()) + + def set_current_screen(self, screen: Screen): + self.current_screen = screen + def install_update(self): - self.current_screen = Screen.PROGRESS + self.set_current_screen(Screen.PROGRESS) self.progress_value = 0 self.progress_text = "Downloading..." self.show_reboot_button = False @@ -80,14 +89,14 @@ class Updater(Widget): def render_prompt_screen(self, rect: rl.Rectangle): # Title - title_rect = rl.Rectangle(MARGIN + 50, 250, rect.width - MARGIN * 2 - 100, TITLE_FONT_SIZE) + title_rect = rl.Rectangle(MARGIN + 50, 250, rect.width - MARGIN * 2 - 100, TITLE_FONT_SIZE * FONT_SCALE) gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD) # Description desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " + "The download size is approximately 1GB.") - desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * 3) + desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4) gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE) # Buttons at the bottom @@ -96,25 +105,22 @@ class Updater(Widget): # WiFi button wifi_button_rect = rl.Rectangle(MARGIN, button_y, button_width, BUTTON_HEIGHT) - if gui_button(wifi_button_rect, "Connect to Wi-Fi"): - self.current_screen = Screen.WIFI - return # Return to avoid processing other buttons after screen change + self._wifi_button.render(wifi_button_rect) # Install button install_button_rect = rl.Rectangle(MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT) - if gui_button(install_button_rect, "Install", button_style=ButtonStyle.PRIMARY): - self.install_update() - return # Return to avoid further processing after action + self._install_button.render(install_button_rect) def render_wifi_screen(self, rect: rl.Rectangle): # Draw the Wi-Fi manager UI - wifi_rect = rl.Rectangle(MARGIN + 50, MARGIN, rect.width - MARGIN * 2 - 100, rect.height - MARGIN * 2 - BUTTON_HEIGHT - 20) - self.wifi_manager_ui.render(wifi_rect) + wifi_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, + rect.height - BUTTON_HEIGHT - MARGIN * 3) + rl.draw_rectangle_rounded(wifi_rect, 0.035, 10, rl.Color(51, 51, 51, 255)) + wifi_content_rect = rl.Rectangle(wifi_rect.x + 50, wifi_rect.y, wifi_rect.width - 100, wifi_rect.height) + self.wifi_manager_ui.render(wifi_content_rect) back_button_rect = rl.Rectangle(MARGIN, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) - if gui_button(back_button_rect, "Back"): - self.current_screen = Screen.PROMPT - return # Return to avoid processing other interactions after screen change + self._back_button.render(back_button_rect) def render_progress_screen(self, rect: rl.Rectangle): title_rect = rl.Rectangle(MARGIN + 100, 330, rect.width - MARGIN * 2 - 200, 100) @@ -133,10 +139,7 @@ class Updater(Widget): # Show reboot button if needed if self.show_reboot_button: reboot_rect = rl.Rectangle(MARGIN + 100, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) - if gui_button(reboot_rect, "Reboot"): - # Return True to signal main loop to exit before rebooting - HARDWARE.reboot() - return + self._reboot_button.render(reboot_rect) def _render(self, rect: rl.Rectangle): if self.current_screen == Screen.PROMPT: @@ -158,8 +161,9 @@ def main(): try: gui_app.init_window("System Update") updater = Updater(updater_path, manifest_path) - for _ in gui_app.render(): - updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + for should_render in gui_app.render(): + if should_render: + updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: # Make sure we clean up even if there's an error gui_app.close() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index cca2cec7ad..562cde39e1 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -14,13 +14,14 @@ class DialogResult(IntEnum): class Widget(abc.ABC): def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) - self._parent_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) - self._is_pressed = [False] * MAX_TOUCH_SLOTS + self._parent_rect: rl.Rectangle | None = None + self.__is_pressed = [False] * MAX_TOUCH_SLOTS # if current mouse/touch down started within the widget's rectangle - self._tracking_is_pressed = [False] * MAX_TOUCH_SLOTS + self.__tracking_is_pressed = [False] * MAX_TOUCH_SLOTS self._enabled: bool | Callable[[], bool] = True self._is_visible: bool | Callable[[], bool] = True self._touch_valid_callback: Callable[[], bool] | None = None + self._click_callback: Callable[[], None] | None = None self._multi_touch = False @property @@ -40,7 +41,7 @@ class Widget(abc.ABC): @property def is_pressed(self) -> bool: - return any(self._is_pressed) + return any(self.__is_pressed) @property def enabled(self) -> bool: @@ -56,6 +57,10 @@ class Widget(abc.ABC): def set_visible(self, visible: bool | Callable[[], bool]) -> None: self._is_visible = visible + def set_click_callback(self, click_callback: Callable[[], None] | None) -> None: + """Set a callback to be called when the widget is clicked.""" + self._click_callback = click_callback + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: """Set a callback to determine if the widget can be clicked.""" self._touch_valid_callback = touch_callback @@ -70,6 +75,13 @@ class Widget(abc.ABC): if changed: self._update_layout_rects() + @property + def _hit_rect(self) -> rl.Rectangle: + # restrict touches to within parent rect if set, useful inside Scroller + if self._parent_rect is None: + return self._rect + return rl.get_collision_rec(self._rect, self._parent_rect) + def render(self, rect: rl.Rectangle = None) -> bool | int | None: if rect is not None: self.set_rect(rect) @@ -90,29 +102,30 @@ class Widget(abc.ABC): # Ignores touches/presses that start outside our rect # Allows touch to leave the rect and come back in focus if mouse did not release if mouse_event.left_pressed and self._touch_valid(): - if rl.check_collision_point_rec(mouse_event.pos, self._rect): - self._is_pressed[mouse_event.slot] = True - self._tracking_is_pressed[mouse_event.slot] = True + if rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): + self._handle_mouse_press(mouse_event.pos) + self.__is_pressed[mouse_event.slot] = True + self.__tracking_is_pressed[mouse_event.slot] = True # Callback such as scroll panel signifies user is scrolling elif not self._touch_valid(): - self._is_pressed[mouse_event.slot] = False - self._tracking_is_pressed[mouse_event.slot] = False + self.__is_pressed[mouse_event.slot] = False + self.__tracking_is_pressed[mouse_event.slot] = False elif mouse_event.left_released: - if self._is_pressed[mouse_event.slot] and rl.check_collision_point_rec(mouse_event.pos, self._rect): + if self.__is_pressed[mouse_event.slot] and rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): self._handle_mouse_release(mouse_event.pos) - self._is_pressed[mouse_event.slot] = False - self._tracking_is_pressed[mouse_event.slot] = False + self.__is_pressed[mouse_event.slot] = False + self.__tracking_is_pressed[mouse_event.slot] = False # Mouse/touch is still within our rect - elif rl.check_collision_point_rec(mouse_event.pos, self._rect): - if self._tracking_is_pressed[mouse_event.slot]: - self._is_pressed[mouse_event.slot] = True + elif rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): + if self.__tracking_is_pressed[mouse_event.slot]: + self.__is_pressed[mouse_event.slot] = True # Mouse/touch left our rect but may come back into focus later - elif not rl.check_collision_point_rec(mouse_event.pos, self._rect): - self._is_pressed[mouse_event.slot] = False + elif not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): + self.__is_pressed[mouse_event.slot] = False return ret @@ -126,8 +139,14 @@ class Widget(abc.ABC): def _update_layout_rects(self) -> None: """Optionally update any layout rects on Widget rect change.""" + def _handle_mouse_press(self, mouse_pos: MousePos) -> bool: + """Optionally handle mouse press events.""" + return False + def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: """Optionally handle mouse release events.""" + if self._click_callback: + self._click_callback() return False def show_event(self): diff --git a/system/ui/widgets/button.py b/system/ui/widgets/button.py index a5dab19789..df7a52d1c0 100644 --- a/system/ui/widgets/button.py +++ b/system/ui/widgets/button.py @@ -3,10 +3,9 @@ from enum import IntEnum import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.application import FontWeight, MousePos from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import TextAlignment, Label +from openpilot.system.ui.widgets.label import Label class ButtonStyle(IntEnum): @@ -14,6 +13,8 @@ class ButtonStyle(IntEnum): PRIMARY = 1 # For main actions DANGER = 2 # For critical actions, like reboot or delete TRANSPARENT = 3 # For buttons with transparent background and border + TRANSPARENT_WHITE_TEXT = 9 # For buttons with transparent background and border and white text + TRANSPARENT_WHITE_BORDER = 10 # For buttons with transparent background and white border and text ACTION = 4 LIST_ACTION = 5 # For list items with action buttons NO_EFFECT = 6 @@ -23,8 +24,6 @@ class ButtonStyle(IntEnum): ICON_PADDING = 15 DEFAULT_BUTTON_FONT_SIZE = 60 -BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51) -BUTTON_DISABLED_BACKGROUND_COLOR = rl.Color(51, 51, 51, 255) ACTION_BUTTON_FONT_SIZE = 48 BUTTON_TEXT_COLOR = { @@ -32,6 +31,8 @@ BUTTON_TEXT_COLOR = { ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255), ButtonStyle.DANGER: rl.Color(228, 228, 228, 255), ButtonStyle.TRANSPARENT: rl.BLACK, + ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.WHITE, + ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.Color(228, 228, 228, 255), ButtonStyle.ACTION: rl.BLACK, ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255), ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255), @@ -39,11 +40,17 @@ BUTTON_TEXT_COLOR = { ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255), } +BUTTON_DISABLED_TEXT_COLORS = { + ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.WHITE, +} + BUTTON_BACKGROUND_COLORS = { ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255), ButtonStyle.PRIMARY: rl.Color(70, 91, 234, 255), - ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), + ButtonStyle.DANGER: rl.Color(226, 44, 44, 255), ButtonStyle.TRANSPARENT: rl.BLACK, + ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, + ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.BLACK, ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255), ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), @@ -56,6 +63,8 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.PRIMARY: rl.Color(48, 73, 244, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.TRANSPARENT: rl.BLACK, + ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, + ButtonStyle.TRANSPARENT_WHITE_BORDER: rl.BLANK, ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74), ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), @@ -63,116 +72,23 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255), } -_pressed_buttons: set[str] = set() # Track mouse press state globally - - -# TODO: This should be a Widget class - -def gui_button( - rect: rl.Rectangle, - text: str, - font_size: int = DEFAULT_BUTTON_FONT_SIZE, - font_weight: FontWeight = FontWeight.MEDIUM, - button_style: ButtonStyle = ButtonStyle.NORMAL, - is_enabled: bool = True, - border_radius: int = 10, # Corner rounding in pixels - text_alignment: TextAlignment = TextAlignment.CENTER, - text_padding: int = 20, # Padding for left/right alignment - icon=None, -) -> int: - button_id = f"{rect.x}_{rect.y}_{rect.width}_{rect.height}" - result = 0 - - if button_style in (ButtonStyle.PRIMARY, ButtonStyle.DANGER) and not is_enabled: - button_style = ButtonStyle.NORMAL - - if button_style == ButtonStyle.ACTION and font_size == DEFAULT_BUTTON_FONT_SIZE: - font_size = ACTION_BUTTON_FONT_SIZE - - # Set background color based on button type - bg_color = BUTTON_BACKGROUND_COLORS[button_style] - mouse_over = is_enabled and rl.check_collision_point_rec(rl.get_mouse_position(), rect) - is_pressed = button_id in _pressed_buttons - - if mouse_over: - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): - # Only this button enters pressed state - _pressed_buttons.add(button_id) - is_pressed = True - - # Use pressed color when mouse is down over this button - if is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): - bg_color = BUTTON_PRESSED_BACKGROUND_COLORS[button_style] - - # Handle button click - if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and is_pressed: - result = 1 - _pressed_buttons.remove(button_id) - - # Clean up pressed state if mouse is released anywhere - if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and button_id in _pressed_buttons: - _pressed_buttons.remove(button_id) - - # Draw the button with rounded corners - roundness = border_radius / (min(rect.width, rect.height) / 2) - if button_style != ButtonStyle.TRANSPARENT: - rl.draw_rectangle_rounded(rect, roundness, 20, bg_color) - else: - rl.draw_rectangle_rounded(rect, roundness, 20, rl.BLACK) - rl.draw_rectangle_rounded_lines_ex(rect, roundness, 20, 2, rl.WHITE) - - # Handle icon and text positioning - font = gui_app.font(font_weight) - text_size = measure_text_cached(font, text, font_size) - text_pos = rl.Vector2(0, rect.y + (rect.height - text_size.y) // 2) # Vertical centering - - # Draw icon if provided - if icon: - icon_y = rect.y + (rect.height - icon.height) / 2 - if text: - if text_alignment == TextAlignment.LEFT: - icon_x = rect.x + text_padding - text_pos.x = icon_x + icon.width + ICON_PADDING - elif text_alignment == TextAlignment.CENTER: - total_width = icon.width + ICON_PADDING + text_size.x - icon_x = rect.x + (rect.width - total_width) / 2 - text_pos.x = icon_x + icon.width + ICON_PADDING - else: # RIGHT - text_pos.x = rect.x + rect.width - text_size.x - text_padding - icon_x = text_pos.x - ICON_PADDING - icon.width - else: - # Center icon when no text - icon_x = rect.x + (rect.width - icon.width) / 2 - - rl.draw_texture_v(icon, rl.Vector2(icon_x, icon_y), rl.WHITE if is_enabled else rl.Color(255, 255, 255, 100)) - else: - # No icon, position text normally - if text_alignment == TextAlignment.LEFT: - text_pos.x = rect.x + text_padding - elif text_alignment == TextAlignment.CENTER: - text_pos.x = rect.x + (rect.width - text_size.x) // 2 - elif text_alignment == TextAlignment.RIGHT: - text_pos.x = rect.x + rect.width - text_size.x - text_padding - - # Draw the button text if any - if text: - color = BUTTON_TEXT_COLOR[button_style] if is_enabled else BUTTON_DISABLED_TEXT_COLOR - rl.draw_text_ex(font, text, text_pos, font_size, 0, color) - - return result +BUTTON_DISABLED_BACKGROUND_COLORS = { + ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, +} class Button(Widget): def __init__(self, - text: str, - click_callback: Callable[[], None] = None, + text: str | Callable[[], str], + click_callback: Callable[[], None] | None = None, font_size: int = DEFAULT_BUTTON_FONT_SIZE, font_weight: FontWeight = FontWeight.MEDIUM, button_style: ButtonStyle = ButtonStyle.NORMAL, border_radius: int = 10, - text_alignment: TextAlignment = TextAlignment.CENTER, + text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, text_padding: int = 20, - icon = None, + icon=None, + elide_right: bool = False, multi_touch: bool = False, ): @@ -181,8 +97,8 @@ class Button(Widget): self._border_radius = border_radius self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] - self._label = Label(text, font_size, font_weight, text_alignment, text_padding, - BUTTON_TEXT_COLOR[self._button_style], icon=icon) + self._label = Label(text, font_size, font_weight, text_alignment, text_padding=text_padding, + text_color=BUTTON_TEXT_COLOR[self._button_style], icon=icon, elide_right=elide_right) self._click_callback = click_callback self._multi_touch = multi_touch @@ -190,9 +106,10 @@ class Button(Widget): def set_text(self, text): self._label.set_text(text) - def _handle_mouse_release(self, mouse_pos: MousePos): - if self._click_callback and self.enabled: - self._click_callback() + def set_button_style(self, button_style: ButtonStyle): + self._button_style = button_style + self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] + self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style]) def _update_state(self): if self.enabled: @@ -202,12 +119,16 @@ class Button(Widget): else: self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] elif self._button_style != ButtonStyle.NO_EFFECT: - self._background_color = BUTTON_DISABLED_BACKGROUND_COLOR - self._label.set_text_color(BUTTON_DISABLED_TEXT_COLOR) + self._background_color = BUTTON_DISABLED_BACKGROUND_COLORS.get(self._button_style, rl.Color(51, 51, 51, 255)) + self._label.set_text_color(BUTTON_DISABLED_TEXT_COLORS.get(self._button_style, rl.Color(228, 228, 228, 51))) def _render(self, _): roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) - rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) + if self._button_style == ButtonStyle.TRANSPARENT_WHITE_BORDER: + rl.draw_rectangle_rounded(self._rect, roundness, 10, rl.BLACK) + rl.draw_rectangle_rounded_lines_ex(self._rect, roundness, 10, 2, rl.WHITE) + else: + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) self._label.render(self._rect) @@ -215,9 +136,9 @@ class ButtonRadio(Button): def __init__(self, text: str, icon, - click_callback: Callable[[], None] = None, + click_callback: Callable[[], None] | None = None, font_size: int = DEFAULT_BUTTON_FONT_SIZE, - text_alignment: TextAlignment = TextAlignment.LEFT, + text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, border_radius: int = 10, text_padding: int = 20, ): @@ -230,9 +151,8 @@ class ButtonRadio(Button): self.selected = False def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) self.selected = not self.selected - if self._click_callback: - self._click_callback() def _update_state(self): if self.selected: @@ -249,3 +169,15 @@ class ButtonRadio(Button): icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 icon_x = self._rect.x + self._rect.width - self._icon.width - self._text_padding - ICON_PADDING rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100)) + + +class IconButton(Widget): + def __init__(self, texture: rl.Texture): + super().__init__() + self._texture = texture + + def _render(self, rect: rl.Rectangle): + color = rl.Color(180, 180, 180, 150) if self.is_pressed else rl.WHITE + draw_x = rect.x + (rect.width - self._texture.width) / 2 + draw_y = rect.y + (rect.height - self._texture.height) / 2 + rl.draw_texture(self._texture, int(draw_x), int(draw_y), color) diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index 1021b5452b..8c5ae0aa01 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -1,28 +1,40 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, Button -from openpilot.system.ui.widgets.label import gui_text_box, Label +from openpilot.system.ui.widgets.button import ButtonStyle, Button +from openpilot.system.ui.widgets.label import Label +from openpilot.system.ui.widgets.html_render import HtmlRenderer, ElementType from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.scroller import Scroller -DIALOG_WIDTH = 1520 -DIALOG_HEIGHT = 600 +OUTER_MARGIN = 200 +RICH_OUTER_MARGIN = 100 BUTTON_HEIGHT = 160 MARGIN = 50 -TEXT_AREA_HEIGHT_REDUCTION = 200 +TEXT_PADDING = 10 BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) + class ConfirmDialog(Widget): - def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel"): + def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False): super().__init__() - self._label = Label(text, 70, FontWeight.BOLD) + if cancel_text is None: + cancel_text = tr("Cancel") + self._label = Label(text, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255)) + self._html_renderer = HtmlRenderer(text=text, text_size={ElementType.P: 50}, center_text=True) self._cancel_button = Button(cancel_text, self._cancel_button_callback) self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY) + self._rich = rich self._dialog_result = DialogResult.NO_ACTION self._cancel_text = cancel_text + self._scroller = Scroller([self._html_renderer], line_separator=False, spacing=0) def set_text(self, text): - self._label.set_text(text) + if not self._rich: + self._label.set_text(text) + else: + self._html_renderer.parse_html_content(text) def reset(self): self._dialog_result = DialogResult.NO_ACTION @@ -34,9 +46,11 @@ class ConfirmDialog(Widget): self._dialog_result = DialogResult.CONFIRM def _render(self, rect: rl.Rectangle): - dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 - dialog_y = (gui_app.height - DIALOG_HEIGHT) / 2 - dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT) + dialog_x = OUTER_MARGIN if not self._rich else RICH_OUTER_MARGIN + dialog_y = OUTER_MARGIN if not self._rich else RICH_OUTER_MARGIN + dialog_width = gui_app.width - 2 * dialog_x + dialog_height = gui_app.height - 2 * dialog_y + dialog_rect = rl.Rectangle(dialog_x, dialog_y, dialog_width, dialog_height) bottom = dialog_rect.y + dialog_rect.height button_width = (dialog_rect.width - 3 * MARGIN) // 2 @@ -48,8 +62,15 @@ class ConfirmDialog(Widget): rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR) - text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y, dialog_rect.width - 2 * MARGIN, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) - self._label.render(text_rect) + text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + TEXT_PADDING, + dialog_rect.width - 2 * MARGIN, dialog_rect.height - BUTTON_HEIGHT - MARGIN - TEXT_PADDING * 2) + if not self._rich: + self._label.render(text_rect) + else: + html_rect = rl.Rectangle(text_rect.x, text_rect.y, text_rect.width, + self._html_renderer.get_total_height(int(text_rect.width))) + self._html_renderer.set_rect(html_rect) + self._scroller.render(text_rect) if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): self._dialog_result = DialogResult.CONFIRM @@ -60,63 +81,14 @@ class ConfirmDialog(Widget): self._confirm_button.render(confirm_button) self._cancel_button.render(cancel_button) else: - centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2 - centered_confirm_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT) - self._confirm_button.render(centered_confirm_button) + full_button_width = dialog_rect.width - 2 * MARGIN + full_confirm_button = rl.Rectangle(dialog_rect.x + MARGIN, button_y, full_button_width, BUTTON_HEIGHT) + self._confirm_button.render(full_confirm_button) return self._dialog_result -def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> DialogResult: - dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 - dialog_y = (gui_app.height - DIALOG_HEIGHT) / 2 - dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT) - # Calculate button positions at the bottom of the dialog - bottom = dialog_rect.y + dialog_rect.height - button_width = (dialog_rect.width - 3 * MARGIN) // 2 - no_button_x = dialog_rect.x + MARGIN - yes_button_x = dialog_rect.x + dialog_rect.width - button_width - MARGIN - button_y = bottom - BUTTON_HEIGHT - MARGIN - no_button = rl.Rectangle(no_button_x, button_y, button_width, BUTTON_HEIGHT) - yes_button = rl.Rectangle(yes_button_x, button_y, button_width, BUTTON_HEIGHT) - - # Draw the dialog background - rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR) - - # Draw the message in the dialog, centered - text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y, dialog_rect.width - 2 * MARGIN, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) - gui_text_box( - text_rect, - message, - font_size=70, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, - font_weight=FontWeight.BOLD, - ) - - # Initialize result; -1 means no action taken yet - result = DialogResult.NO_ACTION - - # Check for keyboard input for accessibility - if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): - result = DialogResult.CONFIRM - elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): - result = DialogResult.CANCEL - - # Check for button clicks - if cancel_text: - if gui_button(yes_button, confirm_text, button_style=ButtonStyle.PRIMARY): - result = DialogResult.CONFIRM - if gui_button(no_button, cancel_text): - result = DialogResult.CANCEL - else: - centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2 - centered_yes_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT) - if gui_button(centered_yes_button, confirm_text, button_style=ButtonStyle.PRIMARY): - result = DialogResult.CONFIRM - - return result - - -def alert_dialog(message: str, button_text: str = "OK") -> DialogResult: - return confirm_dialog(message, button_text, cancel_text="") +def alert_dialog(message: str, button_text: str | None = None): + if button_text is None: + button_text = tr("OK") + return ConfirmDialog(message, button_text, cancel_text="") diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index 247b7a5492..7d90d56925 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -3,11 +3,15 @@ import pyray as rl from dataclasses import dataclass from enum import Enum from typing import Any -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.widgets import Widget, DialogResult -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import Button, ButtonStyle +from openpilot.system.ui.lib.text_measure import measure_text_cached + +LIST_INDENT_PX = 40 class ElementType(Enum): @@ -18,40 +22,80 @@ class ElementType(Enum): H5 = "h5" H6 = "h6" P = "p" + B = "b" + UL = "ul" + LI = "li" BR = "br" +TAG_NAMES = '|'.join([t.value for t in ElementType]) +START_TAG_RE = re.compile(f'<({TAG_NAMES})>') +END_TAG_RE = re.compile(f'') +COMMENT_RE = re.compile(r'', flags=re.DOTALL) +DOCTYPE_RE = re.compile(r']*>') +HTML_BODY_TAGS_RE = re.compile(r']*>') +TOKEN_RE = re.compile(r']+>|<[^>]+>|[^<\s]+') + + +def is_tag(token: str) -> tuple[bool, bool, ElementType | None]: + supported_tag = bool(START_TAG_RE.fullmatch(token)) + supported_end_tag = bool(END_TAG_RE.fullmatch(token)) + tag = ElementType(token[1:-1].strip('/')) if supported_tag or supported_end_tag else None + return supported_tag, supported_end_tag, tag + + @dataclass class HtmlElement: type: ElementType content: str font_size: int font_weight: FontWeight - color: rl.Color margin_top: int margin_bottom: int - line_height: float = 1.2 + line_height: float = 0.9 # matches Qt visually, unsure why not default 1.2 + indent_level: int = 0 class HtmlRenderer(Widget): - def __init__(self, file_path: str): - self.elements: list[HtmlElement] = [] + def __init__(self, file_path: str | None = None, text: str | None = None, + text_size: dict | None = None, text_color: rl.Color = rl.WHITE, center_text: bool = False): + super().__init__() + self._text_color = text_color + self._center_text = center_text self._normal_font = gui_app.font(FontWeight.NORMAL) self._bold_font = gui_app.font(FontWeight.BOLD) - self._scroll_panel = GuiScrollPanel() + self._indent_level = 0 + if text_size is None: + text_size = {} + + self._cached_height: float | None = None + self._cached_width: int = -1 + + # Base paragraph size (Qt stylesheet default is 48px in offroad alerts) + base_p_size = int(text_size.get(ElementType.P, 48)) + + # Untagged text defaults to

self.styles: dict[ElementType, dict[str, Any]] = { - ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 16}, - ElementType.H2: {"size": 60, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 24, "margin_bottom": 12}, - ElementType.H3: {"size": 52, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 10}, - ElementType.H4: {"size": 48, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 16, "margin_bottom": 8}, - ElementType.H5: {"size": 44, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 12, "margin_bottom": 6}, - ElementType.H6: {"size": 40, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 10, "margin_bottom": 4}, - ElementType.P: {"size": 38, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 8, "margin_bottom": 12}, - ElementType.BR: {"size": 0, "weight": FontWeight.NORMAL, "color": rl.BLACK, "margin_top": 0, "margin_bottom": 12}, + ElementType.H1: {"size": round(base_p_size * 2), "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 16}, + ElementType.H2: {"size": round(base_p_size * 1.50), "weight": FontWeight.BOLD, "margin_top": 24, "margin_bottom": 12}, + ElementType.H3: {"size": round(base_p_size * 1.17), "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 10}, + ElementType.H4: {"size": round(base_p_size * 1.00), "weight": FontWeight.BOLD, "margin_top": 16, "margin_bottom": 8}, + ElementType.H5: {"size": round(base_p_size * 0.83), "weight": FontWeight.BOLD, "margin_top": 12, "margin_bottom": 6}, + ElementType.H6: {"size": round(base_p_size * 0.67), "weight": FontWeight.BOLD, "margin_top": 10, "margin_bottom": 4}, + ElementType.P: {"size": base_p_size, "weight": FontWeight.NORMAL, "margin_top": 8, "margin_bottom": 12}, + ElementType.B: {"size": base_p_size, "weight": FontWeight.BOLD, "margin_top": 8, "margin_bottom": 12}, + ElementType.LI: {"size": base_p_size, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 6, "margin_bottom": 6}, + ElementType.BR: {"size": 0, "weight": FontWeight.NORMAL, "margin_top": 0, "margin_bottom": 12}, } - self.parse_html_file(file_path) + self.elements: list[HtmlElement] = [] + if file_path is not None: + self.parse_html_file(file_path) + elif text is not None: + self.parse_html_content(text) + else: + raise ValueError("Either file_path or text must be provided") def parse_html_file(self, file_path: str) -> None: with open(file_path, encoding='utf-8') as file: @@ -60,33 +104,62 @@ class HtmlRenderer(Widget): def parse_html_content(self, html_content: str) -> None: self.elements.clear() + self._cached_height = None + self._cached_width = -1 # Remove HTML comments - html_content = re.sub(r'', '', html_content, flags=re.DOTALL) + html_content = COMMENT_RE.sub('', html_content) # Remove DOCTYPE, html, head, body tags but keep their content - html_content = re.sub(r']*>', '', html_content) - html_content = re.sub(r']*>', '', html_content) + html_content = DOCTYPE_RE.sub('', html_content) + html_content = HTML_BODY_TAGS_RE.sub('', html_content) - # Find all HTML elements - pattern = r'<(h[1-6]|p)(?:[^>]*)>(.*?)|' - matches = re.finditer(pattern, html_content, re.DOTALL | re.IGNORECASE) + # Parse HTML + tokens = TOKEN_RE.findall(html_content) + + def close_tag(): + nonlocal current_content + nonlocal current_tag + + # If no tag is set, default to paragraph so we don't lose text + if current_tag is None: + current_tag = ElementType.P + + text = ' '.join(current_content).strip() + current_content = [] + if text: + if current_tag == ElementType.LI: + text = '• ' + text + self._add_element(current_tag, text) + + current_content: list[str] = [] + current_tag: ElementType | None = None + for token in tokens: + is_start_tag, is_end_tag, tag = is_tag(token) + if tag is not None: + if tag == ElementType.BR: + # Close current tag and add a line break + close_tag() + self._add_element(ElementType.BR, "") + + elif is_start_tag or is_end_tag: + # Always add content regardless of opening or closing tag + close_tag() + + if is_start_tag: + current_tag = tag + else: + current_tag = None + + # increment after we add the content for the current tag + if tag == ElementType.UL: + self._indent_level = self._indent_level + 1 if is_start_tag else max(0, self._indent_level - 1) - for match in matches: - if match.group(0).lower().startswith(' tags - self._add_element(ElementType.BR, "") else: - tag = match.group(1).lower() - content = match.group(2).strip() + current_content.append(token) - # Clean up content - remove extra whitespace - content = re.sub(r'\s+', ' ', content) - content = content.strip() - - if content: # Only add non-empty elements - element_type = ElementType(tag) - self._add_element(element_type, content) + if current_content: + close_tag() def _add_element(self, element_type: ElementType, content: str) -> None: style = self.styles[element_type] @@ -96,42 +169,16 @@ class HtmlRenderer(Widget): content=content, font_size=style["size"], font_weight=style["weight"], - color=style["color"], margin_top=style["margin_top"], margin_bottom=style["margin_bottom"], + indent_level=self._indent_level, ) self.elements.append(element) def _render(self, rect: rl.Rectangle): - margin = 50 - content_rect = rl.Rectangle(rect.x + margin, rect.y + margin, rect.width - (margin * 2), rect.height - (margin * 2)) - - button_height = 160 - button_spacing = 20 - scrollable_height = content_rect.height - button_height - button_spacing - - scrollable_rect = rl.Rectangle(content_rect.x, content_rect.y, content_rect.width, scrollable_height) - - total_height = self.get_total_height(int(scrollable_rect.width)) - scroll_content_rect = rl.Rectangle(scrollable_rect.x, scrollable_rect.y, scrollable_rect.width, total_height) - scroll_offset = self._scroll_panel.handle_scroll(scrollable_rect, scroll_content_rect) - - rl.begin_scissor_mode(int(scrollable_rect.x), int(scrollable_rect.y), int(scrollable_rect.width), int(scrollable_rect.height)) - self._render_content(scrollable_rect, scroll_offset.y) - rl.end_scissor_mode() - - button_width = (rect.width - 3 * 50) // 3 - button_x = content_rect.x + (content_rect.width - button_width) / 2 - button_y = content_rect.y + content_rect.height - button_height - button_rect = rl.Rectangle(button_x, button_y, button_width, button_height) - if gui_button(button_rect, "OK", button_style=ButtonStyle.PRIMARY) == 1: - return DialogResult.CONFIRM - - return DialogResult.NO_ACTION - - def _render_content(self, rect: rl.Rectangle, scroll_offset: float = 0) -> float: - current_y = rect.y + scroll_offset + # TODO: speed up by removing duplicate calculations across renders + current_y = rect.y padding = 20 content_width = rect.width - (padding * 2) @@ -149,23 +196,33 @@ class HtmlRenderer(Widget): wrapped_lines = wrap_text(font, element.content, element.font_size, int(content_width)) for line in wrapped_lines: - if current_y < rect.y - element.font_size: - current_y += element.font_size * element.line_height + # Use FONT_SCALE from wrapped raylib text functions to match what is drawn + if current_y < rect.y - element.font_size * FONT_SCALE: + current_y += element.font_size * FONT_SCALE * element.line_height continue if current_y > rect.y + rect.height: break - rl.draw_text_ex(font, line, rl.Vector2(rect.x + padding, current_y), element.font_size, 0, rl.WHITE) + if self._center_text: + text_width = measure_text_cached(font, line, element.font_size).x + text_x = rect.x + (rect.width - text_width) / 2 + else: # left align + text_x = rect.x + (max(element.indent_level - 1, 0) * LIST_INDENT_PX) - current_y += element.font_size * element.line_height + rl.draw_text_ex(font, line, rl.Vector2(text_x + padding, current_y), element.font_size, 0, self._text_color) + + current_y += element.font_size * FONT_SCALE * element.line_height # Apply bottom margin current_y += element.margin_bottom - return current_y - rect.y - scroll_offset # Return total content height + return current_y - rect.y def get_total_height(self, content_width: int) -> float: + if self._cached_height is not None and self._cached_width == content_width: + return self._cached_height + total_height = 0.0 padding = 20 usable_width = content_width - (padding * 2) @@ -182,13 +239,52 @@ class HtmlRenderer(Widget): wrapped_lines = wrap_text(font, element.content, element.font_size, int(usable_width)) for _ in wrapped_lines: - total_height += element.font_size * element.line_height + total_height += element.font_size * FONT_SCALE * element.line_height total_height += element.margin_bottom + # Store result in cache + self._cached_height = total_height + self._cached_width = content_width + return total_height def _get_font(self, weight: FontWeight): if weight == FontWeight.BOLD: return self._bold_font return self._normal_font + + +class HtmlModal(Widget): + def __init__(self, file_path: str | None = None, text: str | None = None): + super().__init__() + self._content = HtmlRenderer(file_path=file_path, text=text) + self._scroll_panel = GuiScrollPanel() + self._ok_button = Button(tr("OK"), click_callback=lambda: gui_app.set_modal_overlay(None), button_style=ButtonStyle.PRIMARY) + + def _render(self, rect: rl.Rectangle): + margin = 50 + content_rect = rl.Rectangle(rect.x + margin, rect.y + margin, rect.width - (margin * 2), rect.height - (margin * 2)) + + button_height = 160 + button_spacing = 20 + scrollable_height = content_rect.height - button_height - button_spacing + + scrollable_rect = rl.Rectangle(content_rect.x, content_rect.y, content_rect.width, scrollable_height) + + total_height = self._content.get_total_height(int(scrollable_rect.width)) + scroll_content_rect = rl.Rectangle(scrollable_rect.x, scrollable_rect.y, scrollable_rect.width, total_height) + scroll_offset = self._scroll_panel.update(scrollable_rect, scroll_content_rect) + scroll_content_rect.y += scroll_offset + + rl.begin_scissor_mode(int(scrollable_rect.x), int(scrollable_rect.y), int(scrollable_rect.width), int(scrollable_rect.height)) + self._content.render(scroll_content_rect) + rl.end_scissor_mode() + + button_width = (rect.width - 3 * 50) // 3 + button_x = content_rect.x + content_rect.width - button_width + button_y = content_rect.y + content_rect.height - button_height + button_rect = rl.Rectangle(button_x, button_y, button_width, button_height) + self._ok_button.render(button_rect) + + return -1 diff --git a/system/ui/widgets/inputbox.py b/system/ui/widgets/inputbox.py index 239d63037e..f53e3f0ebb 100644 --- a/system/ui/widgets/inputbox.py +++ b/system/ui/widgets/inputbox.py @@ -1,6 +1,6 @@ import pyray as rl import time -from openpilot.system.ui.lib.application import gui_app, MousePos +from openpilot.system.ui.lib.application import gui_app, MousePos, FONT_SCALE from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -130,7 +130,7 @@ class InputBox(Widget): rl.draw_text_ex( font, display_text, - rl.Vector2(int(rect.x + padding - self._text_offset), int(rect.y + rect.height / 2 - font_size / 2)), + rl.Vector2(int(rect.x + padding - self._text_offset), int(rect.y + rect.height / 2 - font_size * FONT_SCALE / 2)), font_size, 0, text_color, @@ -145,7 +145,7 @@ class InputBox(Widget): # Apply text offset to cursor position cursor_x -= self._text_offset - cursor_height = font_size + 4 + cursor_height = font_size * FONT_SCALE + 4 cursor_y = rect.y + rect.height / 2 - cursor_height / 2 rl.draw_line(int(cursor_x), int(cursor_y), int(cursor_x), int(cursor_y + cursor_height), rl.WHITE) diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 25843d1ce8..4ec92f507a 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -5,10 +5,11 @@ from typing import Literal import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.inputbox import InputBox -from openpilot.system.ui.widgets.label import Label, TextAlignment +from openpilot.system.ui.widgets.label import Label KEY_FONT_SIZE = 96 DOUBLE_CLICK_THRESHOLD = 0.5 # seconds @@ -19,7 +20,7 @@ DELETE_REPEAT_INTERVAL = 0.07 CONTENT_MARGIN = 50 BACKSPACE_KEY = "<-" ENTER_KEY = "->" -SPACE_KEY = " " +SPACE_KEY = " " SHIFT_INACTIVE_KEY = "SHIFT_OFF" SHIFT_ACTIVE_KEY = "SHIFT_ON" CAPS_LOCK_KEY = "CAPS" @@ -44,13 +45,13 @@ KEYBOARD_LAYOUTS = { "numbers": [ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], ["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""], - [SYMBOL_KEY, ".", ",", "?", "!", "`", BACKSPACE_KEY], + [SYMBOL_KEY, "_", ",", "?", "!", "`", BACKSPACE_KEY], [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], ], "specials": [ ["[", "]", "{", "}", "#", "%", "^", "*", "+", "="], ["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"], - [NUMERIC_KEY, ".", ",", "?", "!", "'", BACKSPACE_KEY], + [NUMERIC_KEY, "-", ",", "?", "!", "'", BACKSPACE_KEY], [ABC_KEY, SPACE_KEY, ".", ENTER_KEY], ], } @@ -62,8 +63,8 @@ class Keyboard(Widget): self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False self._last_shift_press_time = 0 - self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT) - self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT) + self._title = Label("", 90, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._sub_title = Label("", 55, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._max_text_size = max_text_size self._min_text_size = min_text_size @@ -77,7 +78,7 @@ class Keyboard(Widget): self._backspace_last_repeat: float = 0.0 self._render_return_status = -1 - self._cancel_button = Button("Cancel", self._cancel_button_callback) + self._cancel_button = Button(lambda: tr("Cancel"), self._cancel_button_callback) self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT) @@ -98,12 +99,15 @@ class Keyboard(Widget): if key in self._key_icons: texture = self._key_icons[key] self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture, - button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True) + button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True) else: self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True) self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], button_style=ButtonStyle.KEYBOARD, multi_touch=True) + def set_text(self, text: str): + self._input_box.text = text + @property def text(self): return self._input_box.text @@ -243,8 +247,14 @@ class Keyboard(Widget): if not self._caps_lock and self._layout_name == "uppercase": self._layout_name = "lowercase" - def reset(self): + def reset(self, min_text_size: int | None = None): + if min_text_size is not None: + self._min_text_size = min_text_size self._render_return_status = -1 + self._last_shift_press_time = 0 + self._backspace_pressed = False + self._backspace_press_time = 0.0 + self._backspace_last_repeat = 0.0 self.clear() diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index 2da9a9f8df..7d76802565 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -1,9 +1,9 @@ -from enum import IntEnum +from collections.abc import Callable from itertools import zip_longest - +from typing import Union import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR +from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.utils import GuiStyleContext from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex @@ -12,10 +12,13 @@ from openpilot.system.ui.widgets import Widget ICON_PADDING = 15 -class TextAlignment(IntEnum): - LEFT = 0 - CENTER = 1 - RIGHT = 2 + +# TODO: make this common +def _resolve_value(value, default=""): + if callable(value): + return value() + return value if value is not None else default + # TODO: This should be a Widget class def gui_label( @@ -34,17 +37,17 @@ def gui_label( # Elide text to fit within the rectangle if elide_right and text_size.x > rect.width: - ellipsis = "..." + _ellipsis = "..." left, right = 0, len(text) while left < right: mid = (left + right) // 2 - candidate = text[:mid] + ellipsis + candidate = text[:mid] + _ellipsis candidate_size = measure_text_cached(font, candidate, font_size) if candidate_size.x <= rect.width: left = mid + 1 else: right = mid - display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis + display_text = text[: left - 1] + _ellipsis if left > 0 else _ellipsis text_size = measure_text_cached(font, display_text, font_size) # Calculate horizontal position based on alignment @@ -76,8 +79,8 @@ def gui_text_box( ): styles = [ (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, font_size), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, font_size), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, round(font_size * FONT_SCALE)), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, round(font_size * FONT_SCALE)), (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) @@ -95,13 +98,15 @@ def gui_text_box( # Non-interactive text area. Can render emojis and an optional specified icon. class Label(Widget): def __init__(self, - text: str, + text: str | Callable[[], str], font_size: int = DEFAULT_TEXT_SIZE, font_weight: FontWeight = FontWeight.NORMAL, - text_alignment: TextAlignment = TextAlignment.CENTER, - text_padding: int = 20, + text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + text_alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + text_padding: int = 0, text_color: rl.Color = DEFAULT_TEXT_COLOR, - icon = None, + icon: Union[rl.Texture, None] = None, # noqa: UP007 + elide_right: bool = False, ): super().__init__() @@ -109,41 +114,79 @@ class Label(Widget): self._font = gui_app.font(self._font_weight) self._font_size = font_size self._text_alignment = text_alignment + self._text_alignment_vertical = text_alignment_vertical self._text_padding = text_padding self._text_color = text_color self._icon = icon + self._elide_right = elide_right + + self._text = text self.set_text(text) def set_text(self, text): - self._text_raw = text - self._update_text(self._text_raw) + self._text = text + self._update_text(self._text) def set_text_color(self, color): self._text_color = color - def _update_layout_rects(self): - self._update_text(self._text_raw) + def set_font_size(self, size): + self._font_size = size + self._update_text(self._text) def _update_text(self, text): self._emojis = [] self._text_size = [] - self._text = wrap_text(self._font, text, self._font_size, self._rect.width - (self._text_padding*2)) - for t in self._text: + text = _resolve_value(text) + + if self._elide_right: + display_text = text + + # Elide text to fit within the rectangle + text_size = measure_text_cached(self._font, text, self._font_size) + content_width = self._rect.width - self._text_padding * 2 + if self._icon: + content_width -= self._icon.width + ICON_PADDING + if text_size.x > content_width: + _ellipsis = "..." + left, right = 0, len(text) + while left < right: + mid = (left + right) // 2 + candidate = text[:mid] + _ellipsis + candidate_size = measure_text_cached(self._font, candidate, self._font_size) + if candidate_size.x <= content_width: + left = mid + 1 + else: + right = mid + display_text = text[: left - 1] + _ellipsis if left > 0 else _ellipsis + + self._text_wrapped = [display_text] + else: + self._text_wrapped = wrap_text(self._font, text, self._font_size, round(self._rect.width - (self._text_padding * 2))) + + for t in self._text_wrapped: self._emojis.append(find_emoji(t)) self._text_size.append(measure_text_cached(self._font, t, self._font_size)) def _render(self, _): - text = self._text[0] if self._text else None + # Text can be a callable + # TODO: cache until text changed + self._update_text(self._text) + text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0) - text_pos = rl.Vector2(0, (self._rect.y + (self._rect.height - (text_size.y)) // 2)) + if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: + total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE + text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2)) + else: + text_pos = rl.Vector2(self._rect.x, self._rect.y) if self._icon: icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 - if text: - if self._text_alignment == TextAlignment.LEFT: + if len(self._text_wrapped) > 0: + if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: icon_x = self._rect.x + self._text_padding text_pos.x = self._icon.width + ICON_PADDING - elif self._text_alignment == TextAlignment.CENTER: + elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: total_width = self._icon.width + ICON_PADDING + text_size.x icon_x = self._rect.x + (self._rect.width - total_width) / 2 text_pos.x = self._icon.width + ICON_PADDING @@ -153,14 +196,14 @@ class Label(Widget): icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2 rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE) - for text, text_size, emojis in zip_longest(self._text, self._text_size, self._emojis, fillvalue=[]): + for text, text_size, emojis in zip_longest(self._text_wrapped, self._text_size, self._emojis, fillvalue=[]): line_pos = rl.Vector2(text_pos.x, text_pos.y) - if self._text_alignment == TextAlignment.LEFT: - line_pos.x += self._rect.x + self._text_padding - elif self._text_alignment == TextAlignment.CENTER: - line_pos.x += self._rect.x + (self._rect.width - text_size.x) // 2 - elif self._text_alignment == TextAlignment.RIGHT: - line_pos.x += self._rect.x + self._rect.width - text_size.x - self._text_padding + if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + line_pos.x += self._text_padding + elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + line_pos.x += (self._rect.width - text_size.x) // 2 + elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + line_pos.x += self._rect.width - text_size.x - self._text_padding prev_index = 0 for start, end, emoji in emojis: @@ -170,8 +213,8 @@ class Label(Widget): line_pos.x += width_before.x tex = emoji_tex(emoji) - rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height, self._text_color) - line_pos.x += self._font_size + rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height * FONT_SCALE, self._text_color) + line_pos.x += self._font_size * FONT_SCALE prev_index = end rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color) - text_pos.y += text_size.y or self._font_size + text_pos.y += text_size.y or self._font_size * FONT_SCALE diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index a8d81a8ba2..cfb8ab58a3 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -3,17 +3,20 @@ import pyray as rl from collections.abc import Callable from abc import ABC from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT +from openpilot.system.ui.widgets.label import gui_label +from openpilot.system.ui.widgets.html_render import HtmlRenderer, ElementType ITEM_BASE_WIDTH = 600 ITEM_BASE_HEIGHT = 170 ITEM_PADDING = 20 ITEM_TEXT_FONT_SIZE = 50 ITEM_TEXT_COLOR = rl.WHITE +ITEM_TEXT_VALUE_COLOR = rl.Color(170, 170, 170, 255) ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255) ITEM_DESC_FONT_SIZE = 40 ITEM_DESC_V_OFFSET = 140 @@ -41,16 +44,23 @@ class ItemAction(Widget, ABC): self.set_rect(rl.Rectangle(0, 0, width, 0)) self._enabled_source = enabled + def get_width_hint(self) -> float: + # Return's action ideal width, 0 means use full width + return self._rect.width + + def set_enabled(self, enabled: bool | Callable[[], bool]): + self._enabled_source = enabled + @property def enabled(self): return _resolve_value(self._enabled_source, False) class ToggleAction(ItemAction): - def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True): + def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True, + callback: Callable[[bool], None] | None = None): super().__init__(width, enabled) - self.toggle = Toggle(initial_state=initial_state) - self.state = initial_state + self.toggle = Toggle(initial_state=initial_state, callback=callback) def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: super().set_touch_valid_callback(touch_callback) @@ -58,36 +68,81 @@ class ToggleAction(ItemAction): def _render(self, rect: rl.Rectangle) -> bool: self.toggle.set_enabled(self.enabled) - self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self._rect.width, TOGGLE_HEIGHT)) - return False + clicked = self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self._rect.width, TOGGLE_HEIGHT)) + return bool(clicked) def set_state(self, state: bool): - self.state = state self.toggle.set_state(state) def get_state(self) -> bool: - return self.state + return self.toggle.get_state() class ButtonAction(ItemAction): def __init__(self, text: str | Callable[[], str], width: int = BUTTON_WIDTH, enabled: bool | Callable[[], bool] = True): super().__init__(width, enabled) self._text_source = text + self._value_source: str | Callable[[], str] | None = None + self._pressed = False + self._font = gui_app.font(FontWeight.NORMAL) + + def pressed(): + self._pressed = True + + self._button = Button( + self.text, + font_size=BUTTON_FONT_SIZE, + font_weight=BUTTON_FONT_WEIGHT, + button_style=ButtonStyle.LIST_ACTION, + border_radius=BUTTON_BORDER_RADIUS, + click_callback=pressed, + text_padding=0, + ) + self.set_enabled(enabled) + + def get_width_hint(self) -> float: + value_text = self.value + if value_text: + text_width = measure_text_cached(self._font, value_text, ITEM_TEXT_FONT_SIZE).x + return text_width + BUTTON_WIDTH + TEXT_PADDING + else: + return BUTTON_WIDTH + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + super().set_touch_valid_callback(touch_callback) + self._button.set_touch_valid_callback(touch_callback) + + def set_text(self, text: str | Callable[[], str]): + self._text_source = text + + def set_value(self, value: str | Callable[[], str]): + self._value_source = value @property def text(self): - return _resolve_value(self._text_source, "Error") + return _resolve_value(self._text_source, tr("Error")) + + @property + def value(self): + return _resolve_value(self._value_source, "") def _render(self, rect: rl.Rectangle) -> bool: - return gui_button( - rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT), - self.text, - border_radius=BUTTON_BORDER_RADIUS, - font_weight=BUTTON_FONT_WEIGHT, - font_size=BUTTON_FONT_SIZE, - button_style=ButtonStyle.LIST_ACTION, - is_enabled=self.enabled, - ) == 1 + self._button.set_text(self.text) + self._button.set_enabled(_resolve_value(self.enabled)) + button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT) + self._button.render(button_rect) + + value_text = self.value + if value_text: + value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height) + gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR, + font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + + # TODO: just use the generic Widget click callbacks everywhere, no returning from render + pressed = self._pressed + self._pressed = False + return pressed class TextAction(ItemAction): @@ -102,30 +157,35 @@ class TextAction(ItemAction): @property def text(self): - return _resolve_value(self._text_source, "Error") + return _resolve_value(self._text_source, tr("Error")) + + def get_width_hint(self) -> float: + text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x + return text_width + TEXT_PADDING def _render(self, rect: rl.Rectangle) -> bool: - current_text = self.text - text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE) - - text_x = rect.x + (rect.width - text_size.x) / 2 - text_y = rect.y + (rect.height - text_size.y) / 2 - rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color) + gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color, + font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) return False - def get_width(self) -> int: - text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x - return int(text_width + TEXT_PADDING) + def set_text(self, text: str | Callable[[], str]): + self._text_source = text class DualButtonAction(ItemAction): - def __init__(self, left_text: str, right_text: str, left_callback: Callable = None, + def __init__(self, left_text: str | Callable[[], str], right_text: str | Callable[[], str], left_callback: Callable = None, right_callback: Callable = None, enabled: bool | Callable[[], bool] = True): super().__init__(width=0, enabled=enabled) # Width 0 means use full width - self.left_text, self.right_text = left_text, right_text - self.left_callback, self.right_callback = left_callback, right_callback + self.left_button = Button(left_text, click_callback=left_callback, button_style=ButtonStyle.NORMAL, text_padding=0) + self.right_button = Button(right_text, click_callback=right_callback, button_style=ButtonStyle.DANGER, text_padding=0) - def _render(self, rect: rl.Rectangle) -> bool: + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + super().set_touch_valid_callback(touch_callback) + self.left_button.set_touch_valid_callback(touch_callback) + self.right_button.set_touch_valid_callback(touch_callback) + + def _render(self, rect: rl.Rectangle): button_spacing = 30 button_height = 120 button_width = (rect.width - button_spacing) / 2 @@ -134,40 +194,45 @@ class DualButtonAction(ItemAction): left_rect = rl.Rectangle(rect.x, button_y, button_width, button_height) right_rect = rl.Rectangle(rect.x + button_width + button_spacing, button_y, button_width, button_height) - left_clicked = gui_button(left_rect, self.left_text, button_style=ButtonStyle.LIST_ACTION) == 1 - right_clicked = gui_button(right_rect, self.right_text, button_style=ButtonStyle.DANGER) == 1 + # expand one to full width if other is not visible + if not self.left_button.is_visible: + right_rect.x = rect.x + right_rect.width = rect.width + elif not self.right_button.is_visible: + left_rect.width = rect.width - if left_clicked and self.left_callback: - self.left_callback() - return True - if right_clicked and self.right_callback: - self.right_callback() - return True - return False + # Render buttons + self.left_button.render(left_rect) + self.right_button.render(right_rect) class MultipleButtonAction(ItemAction): - def __init__(self, buttons: list[str], button_width: int, selected_index: int = 0, callback: Callable = None): - super().__init__(width=len(buttons) * (button_width + 20), enabled=True) + def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable = None): + super().__init__(width=len(buttons) * button_width + (len(buttons) - 1) * RIGHT_ITEM_PADDING, enabled=True) self.buttons = buttons self.button_width = button_width self.selected_button = selected_index self.callback = callback self._font = gui_app.font(FontWeight.MEDIUM) - def _render(self, rect: rl.Rectangle) -> bool: - spacing = 20 - button_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2 - clicked = -1 + def set_selected_button(self, index: int): + if 0 <= index < len(self.buttons): + self.selected_button = index - for i, text in enumerate(self.buttons): + def get_selected_button(self) -> int: + return self.selected_button + + def _render(self, rect: rl.Rectangle): + spacing = RIGHT_ITEM_PADDING + button_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2 + + for i, _text in enumerate(self.buttons): button_x = rect.x + i * (self.button_width + spacing) button_rect = rl.Rectangle(button_x, button_y, self.button_width, BUTTON_HEIGHT) # Check button state mouse_pos = rl.get_mouse_position() - is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect) - is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed + is_pressed = rl.check_collision_point_rec(mouse_pos, button_rect) and self.enabled and self.is_pressed is_selected = i == self.selected_button # Button colors @@ -178,48 +243,60 @@ class MultipleButtonAction(ItemAction): else: bg_color = rl.Color(57, 57, 57, 255) # Gray + if not self.enabled: + bg_color = rl.Color(bg_color.r, bg_color.g, bg_color.b, 150) # Dim + # Draw button rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color) # Draw text + text = _resolve_value(_text, "") text_size = measure_text_cached(self._font, text, 40) text_x = button_x + (self.button_width - text_size.x) / 2 text_y = button_y + (BUTTON_HEIGHT - text_size.y) / 2 - rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255)) + text_color = rl.Color(228, 228, 228, 255) if self.enabled else rl.Color(150, 150, 150, 255) + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color) - # Handle click - if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed: - clicked = i - - if clicked >= 0: - self.selected_button = clicked - if self.callback: - self.callback(clicked) - return True - return False + def _handle_mouse_release(self, mouse_pos: MousePos): + spacing = RIGHT_ITEM_PADDING + button_y = self._rect.y + (self._rect.height - BUTTON_HEIGHT) / 2 + for i, _ in enumerate(self.buttons): + button_x = self._rect.x + i * (self.button_width + spacing) + button_rect = rl.Rectangle(button_x, button_y, self.button_width, BUTTON_HEIGHT) + if rl.check_collision_point_rec(mouse_pos, button_rect): + self.selected_button = i + if self.callback: + self.callback(i) class ListItem(Widget): - def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None, + def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None, description_visible: bool = False, callback: Callable | None = None, action_item: ItemAction | None = None): super().__init__() - self.title = title - self.icon = icon - self.description = description + self._title = title + self.set_icon(icon) + self._description = description self.description_visible = description_visible self.callback = callback + self.description_opened_callback: Callable | None = None self.action_item = action_item self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT)) self._font = gui_app.font(FontWeight.NORMAL) - self._icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) if self.icon else None + + self._html_renderer = HtmlRenderer(text="", text_size={ElementType.P: ITEM_DESC_FONT_SIZE}, + text_color=ITEM_DESC_TEXT_COLOR) + self._parse_description(self.description) # Cached properties for performance - self._prev_max_width: int = 0 - self._wrapped_description: str | None = None - self._prev_description: str | None = None - self._description_height: float = 0 + self._prev_description: str | None = self.description + + def show_event(self): + self._set_description_visible(False) + + def set_description_opened_callback(self, callback: Callable) -> None: + self.description_opened_callback = callback def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: super().set_touch_valid_callback(touch_callback) @@ -241,11 +318,26 @@ class ListItem(Widget): # Click was on right item, don't toggle description return - if self.description: - self.description_visible = not self.description_visible - content_width = self.get_content_width(int(self._rect.width - ITEM_PADDING * 2)) + self._set_description_visible(not self.description_visible) + + def _set_description_visible(self, visible: bool): + if self.description and self.description_visible != visible: + self.description_visible = visible + # do callback first in case receiver changes description + if self.description_visible and self.description_opened_callback is not None: + self.description_opened_callback() + # Call _update_state to catch any description changes + self._update_state() + + content_width = int(self._rect.width - ITEM_PADDING * 2) self._rect.height = self.get_item_height(self._font, content_width) + def _update_state(self): + # Detect changes if description is callback + new_description = self.description + if new_description != self._prev_description: + self._parse_description(new_description) + def _render(self, _): if not self.is_visible: return @@ -262,7 +354,7 @@ class ListItem(Widget): if self.title: # Draw icon if present if self.icon: - rl.draw_texture(self._icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - self._icon_texture.width) // 2), rl.WHITE) + rl.draw_texture(self._icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - self._icon_texture.height) // 2), rl.WHITE) text_x += ICON_SIZE + ITEM_PADDING # Draw main text @@ -271,16 +363,16 @@ class ListItem(Widget): rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR) # Draw description if visible - current_description = self.get_description() - if self.description_visible and current_description and self._wrapped_description: - rl.draw_text_ex( - self._font, - self._wrapped_description, - rl.Vector2(text_x, self._rect.y + ITEM_DESC_V_OFFSET), - ITEM_DESC_FONT_SIZE, - 0, - ITEM_DESC_TEXT_COLOR, + if self.description_visible: + content_width = int(self._rect.width - ITEM_PADDING * 2) + description_height = self._html_renderer.get_total_height(content_width) + description_rect = rl.Rectangle( + self._rect.x + ITEM_PADDING, + self._rect.y + ITEM_DESC_V_OFFSET, + content_width, + description_height ) + self._html_renderer.render(description_rect) # Draw right item if present if self.action_item: @@ -291,78 +383,84 @@ class ListItem(Widget): if self.callback: self.callback() - def get_description(self): - return _resolve_value(self.description, None) + def set_icon(self, icon: str | None): + self.icon = icon + self._icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) if self.icon else None + + def set_description(self, description: str | Callable[[], str] | None): + self._description = description + + def _parse_description(self, new_desc): + self._html_renderer.parse_html_content(new_desc) + self._prev_description = new_desc + + @property + def title(self): + return _resolve_value(self._title, "") + + @property + def description(self): + return _resolve_value(self._description, "") def get_item_height(self, font: rl.Font, max_width: int) -> float: if not self.is_visible: return 0 - current_description = self.get_description() - if self.description_visible and current_description: - if ( - not self._wrapped_description - or current_description != self._prev_description - or max_width != self._prev_max_width - ): - self._prev_max_width = max_width - self._prev_description = current_description - - wrapped_lines = wrap_text(font, current_description, ITEM_DESC_FONT_SIZE, max_width) - self._wrapped_description = "\n".join(wrapped_lines) - self._description_height = len(wrapped_lines) * ITEM_DESC_FONT_SIZE + 10 - return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING - return ITEM_BASE_HEIGHT - - def get_content_width(self, total_width: int) -> int: - if self.action_item and self.action_item.rect.width > 0: - return total_width - int(self.action_item.rect.width) - RIGHT_ITEM_PADDING - return total_width + height = float(ITEM_BASE_HEIGHT) + if self.description_visible: + description_height = self._html_renderer.get_total_height(max_width) + height += description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING + return height def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: if not self.action_item: return rl.Rectangle(0, 0, 0, 0) - right_width = self.action_item.rect.width + right_width = self.action_item.get_width_hint() if right_width == 0: # Full width action (like DualButtonAction) return rl.Rectangle(item_rect.x + ITEM_PADDING, item_rect.y, item_rect.width - (ITEM_PADDING * 2), ITEM_BASE_HEIGHT) + # Clip width to available space, never overlapping this Item's title + content_width = item_rect.width - (ITEM_PADDING * 2) + title_width = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE).x + right_width = min(content_width - title_width, right_width) + right_x = item_rect.x + item_rect.width - right_width right_y = item_rect.y return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT) # Factory functions -def simple_item(title: str, callback: Callable | None = None) -> ListItem: +def simple_item(title: str | Callable[[], str], callback: Callable | None = None) -> ListItem: return ListItem(title=title, callback=callback) -def toggle_item(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False, +def toggle_item(title: str | Callable[[], str], description: str | Callable[[], str] | None = None, initial_state: bool = False, callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True) -> ListItem: - action = ToggleAction(initial_state=initial_state, enabled=enabled) - return ListItem(title=title, description=description, action_item=action, icon=icon, callback=callback) + action = ToggleAction(initial_state=initial_state, enabled=enabled, callback=callback) + return ListItem(title=title, description=description, action_item=action, icon=icon) -def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None, +def button_item(title: str | Callable[[], str], button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None, callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem: action = ButtonAction(text=button_text, enabled=enabled) return ListItem(title=title, description=description, action_item=action, callback=callback) -def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None, +def text_item(title: str | Callable[[], str], value: str | Callable[[], str], description: str | Callable[[], str] | None = None, callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem: - action = TextAction(text=value, color=rl.Color(170, 170, 170, 255), enabled=enabled) + action = TextAction(text=value, color=ITEM_TEXT_VALUE_COLOR, enabled=enabled) return ListItem(title=title, description=description, action_item=action, callback=callback) -def dual_button_item(left_text: str, right_text: str, left_callback: Callable = None, right_callback: Callable = None, +def dual_button_item(left_text: str | Callable[[], str], right_text: str | Callable[[], str], left_callback: Callable = None, right_callback: Callable = None, description: str | Callable[[], str] | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem: action = DualButtonAction(left_text, right_text, left_callback, right_callback, enabled) return ListItem(title="", description=description, action_item=action) -def multiple_button_item(title: str, description: str, buttons: list[str], selected_index: int, +def multiple_button_item(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]], selected_index: int, button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""): action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback) return ListItem(title=title, description=description, icon=icon, action_item=action) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 3e6317a49c..592c9de971 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -4,13 +4,26 @@ from typing import cast import pyray as rl from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network +from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.keyboard import Keyboard -from openpilot.system.ui.widgets.label import TextAlignment, gui_label +from openpilot.system.ui.widgets.label import gui_label +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item + +# These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI +try: + from openpilot.common.params import Params + from openpilot.selfdrive.ui.ui_state import ui_state + from openpilot.selfdrive.ui.lib.prime_state import PrimeType +except Exception: + Params = None + ui_state = None # type: ignore + PrimeType = None # type: ignore NM_DEVICE_STATE_NEED_AUTH = 60 MIN_PASSWORD_LENGTH = 8 @@ -26,6 +39,11 @@ STRENGTH_ICONS = [ ] +class PanelType(IntEnum): + WIFI = 0 + ADVANCED = 1 + + class UIState(IntEnum): IDLE = 0 CONNECTING = 1 @@ -34,10 +52,227 @@ class UIState(IntEnum): FORGETTING = 4 +class NavButton(Widget): + def __init__(self, text: str): + super().__init__() + self.text = text + self.set_rect(rl.Rectangle(0, 0, 400, 100)) + + def _render(self, _): + color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255) + rl.draw_rectangle_rounded(self._rect, 0.6, 10, color) + gui_label(self.rect, self.text, font_size=60, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + + +class NetworkUI(Widget): + def __init__(self, wifi_manager: WifiManager): + super().__init__() + self._wifi_manager = wifi_manager + self._current_panel: PanelType = PanelType.WIFI + self._wifi_panel = WifiManagerUI(wifi_manager) + self._advanced_panel = AdvancedNetworkSettings(wifi_manager) + self._nav_button = NavButton(tr("Advanced")) + self._nav_button.set_click_callback(self._cycle_panel) + + def show_event(self): + self._set_current_panel(PanelType.WIFI) + self._wifi_panel.show_event() + + def hide_event(self): + self._wifi_panel.hide_event() + + def _cycle_panel(self): + if self._current_panel == PanelType.WIFI: + self._set_current_panel(PanelType.ADVANCED) + else: + self._set_current_panel(PanelType.WIFI) + + def _render(self, _): + # subtract button + content_rect = rl.Rectangle(self._rect.x, self._rect.y + self._nav_button.rect.height + 40, + self._rect.width, self._rect.height - self._nav_button.rect.height - 40) + if self._current_panel == PanelType.WIFI: + self._nav_button.text = tr("Advanced") + self._nav_button.set_position(self._rect.x + self._rect.width - self._nav_button.rect.width, self._rect.y + 20) + self._wifi_panel.render(content_rect) + else: + self._nav_button.text = tr("Back") + self._nav_button.set_position(self._rect.x, self._rect.y + 20) + self._advanced_panel.render(content_rect) + + self._nav_button.render() + + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + + +class AdvancedNetworkSettings(Widget): + def __init__(self, wifi_manager: WifiManager): + super().__init__() + self._wifi_manager = wifi_manager + self._wifi_manager.add_callbacks(networks_updated=self._on_network_updated) + self._params = Params() + + self._keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) + + # Tethering + self._tethering_action = ToggleAction(initial_state=False) + tethering_btn = ListItem(lambda: tr("Enable Tethering"), action_item=self._tethering_action, callback=self._toggle_tethering) + + # Edit tethering password + self._tethering_password_action = ButtonAction(lambda: tr("EDIT")) + tethering_password_btn = ListItem(lambda: tr("Tethering Password"), action_item=self._tethering_password_action, callback=self._edit_tethering_password) + + # Roaming toggle + roaming_enabled = self._params.get_bool("GsmRoaming") + self._roaming_action = ToggleAction(initial_state=roaming_enabled) + self._roaming_btn = ListItem(lambda: tr("Enable Roaming"), action_item=self._roaming_action, callback=self._toggle_roaming) + + # Cellular metered toggle + cellular_metered = self._params.get_bool("GsmMetered") + self._cellular_metered_action = ToggleAction(initial_state=cellular_metered) + self._cellular_metered_btn = ListItem(lambda: tr("Cellular Metered"), + description=lambda: tr("Prevent large data uploads when on a metered cellular connection"), + action_item=self._cellular_metered_action, callback=self._toggle_cellular_metered) + + # APN setting + self._apn_btn = button_item(lambda: tr("APN Setting"), lambda: tr("EDIT"), callback=self._edit_apn) + + # Wi-Fi metered toggle + self._wifi_metered_action = MultipleButtonAction([lambda: tr("default"), lambda: tr("metered"), lambda: tr("unmetered")], 255, 0, + callback=self._toggle_wifi_metered) + wifi_metered_btn = ListItem(lambda: tr("Wi-Fi Network Metered"), description=lambda: tr("Prevent large data uploads when on a metered Wi-Fi connection"), + action_item=self._wifi_metered_action) + + items: list[Widget] = [ + tethering_btn, + tethering_password_btn, + text_item(lambda: tr("IP Address"), lambda: self._wifi_manager.ipv4_address), + self._roaming_btn, + self._apn_btn, + self._cellular_metered_btn, + wifi_metered_btn, + button_item(lambda: tr("Hidden Network"), lambda: tr("CONNECT"), callback=self._connect_to_hidden_network), + ] + + self._scroller = Scroller(items, line_separator=True, spacing=0) + + # Set initial config + metered = self._params.get_bool("GsmMetered") + self._wifi_manager.update_gsm_settings(roaming_enabled, self._params.get("GsmApn") or "", metered) + + def _on_network_updated(self, networks: list[Network]): + self._tethering_action.set_enabled(True) + self._tethering_action.set_state(self._wifi_manager.is_tethering_active()) + self._tethering_password_action.set_enabled(True) + + if self._wifi_manager.is_tethering_active() or self._wifi_manager.ipv4_address == "": + self._wifi_metered_action.set_enabled(False) + self._wifi_metered_action.selected_button = 0 + elif self._wifi_manager.ipv4_address != "": + metered = self._wifi_manager.current_network_metered + self._wifi_metered_action.set_enabled(True) + self._wifi_metered_action.selected_button = int(metered) if metered in (MeteredType.UNKNOWN, MeteredType.YES, MeteredType.NO) else 0 + + def _toggle_tethering(self): + checked = self._tethering_action.get_state() + self._tethering_action.set_enabled(False) + if checked: + self._wifi_metered_action.set_enabled(False) + self._wifi_manager.set_tethering_active(checked) + + def _toggle_roaming(self): + roaming_state = self._roaming_action.get_state() + self._params.put_bool("GsmRoaming", roaming_state) + self._wifi_manager.update_gsm_settings(roaming_state, self._params.get("GsmApn") or "", self._params.get_bool("GsmMetered")) + + def _edit_apn(self): + def update_apn(result): + if result != 1: + return + + apn = self._keyboard.text.strip() + if apn == "": + self._params.remove("GsmApn") + else: + self._params.put("GsmApn", apn) + + self._wifi_manager.update_gsm_settings(self._params.get_bool("GsmRoaming"), apn, self._params.get_bool("GsmMetered")) + + current_apn = self._params.get("GsmApn") or "" + self._keyboard.reset(min_text_size=0) + self._keyboard.set_title(tr("Enter APN"), tr("leave blank for automatic configuration")) + self._keyboard.set_text(current_apn) + gui_app.set_modal_overlay(self._keyboard, update_apn) + + def _toggle_cellular_metered(self): + metered = self._cellular_metered_action.get_state() + self._params.put_bool("GsmMetered", metered) + self._wifi_manager.update_gsm_settings(self._params.get_bool("GsmRoaming"), self._params.get("GsmApn") or "", metered) + + def _toggle_wifi_metered(self, metered): + metered_type = {0: MeteredType.UNKNOWN, 1: MeteredType.YES, 2: MeteredType.NO}.get(metered, MeteredType.UNKNOWN) + self._wifi_metered_action.set_enabled(False) + self._wifi_manager.set_current_network_metered(metered_type) + + def _connect_to_hidden_network(self): + def connect_hidden(result): + if result != 1: + return + + ssid = self._keyboard.text + if not ssid: + return + + def enter_password(result): + password = self._keyboard.text + if password == "": + # connect without password + self._wifi_manager.connect_to_network(ssid, "", hidden=True) + return + + self._wifi_manager.connect_to_network(ssid, password, hidden=True) + + self._keyboard.reset(min_text_size=0) + self._keyboard.set_title(tr("Enter password"), tr("for \"{}\"").format(ssid)) + gui_app.set_modal_overlay(self._keyboard, enter_password) + + self._keyboard.reset(min_text_size=1) + self._keyboard.set_title(tr("Enter SSID"), "") + gui_app.set_modal_overlay(self._keyboard, connect_hidden) + + def _edit_tethering_password(self): + def update_password(result): + if result != 1: + return + + password = self._keyboard.text + self._wifi_manager.set_tethering_password(password) + self._tethering_password_action.set_enabled(False) + + self._keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH) + self._keyboard.set_title(tr("Enter new tethering password"), "") + self._keyboard.set_text(self._wifi_manager.tethering_password) + gui_app.set_modal_overlay(self._keyboard, update_password) + + def _update_state(self): + self._wifi_manager.process_callbacks() + + # If not using prime SIM, show GSM settings and enable IPv4 forwarding + show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE) + self._wifi_manager.set_ipv4_forward(show_cell_settings) + self._roaming_btn.set_visible(show_cell_settings) + self._apn_btn.set_visible(show_cell_settings) + self._cellular_metered_btn.set_visible(show_cell_settings) + + def _render(self, _): + self._scroller.render(self._rect) + + class WifiManagerUI(Widget): def __init__(self, wifi_manager: WifiManager): super().__init__() - self.wifi_manager = wifi_manager + self._wifi_manager = wifi_manager self.state: UIState = UIState.IDLE self._state_network: Network | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING self._password_retry: bool = False # for NEEDS_AUTH @@ -49,40 +284,41 @@ class WifiManagerUI(Widget): self._networks: list[Network] = [] self._networks_buttons: dict[str, Button] = {} self._forget_networks_buttons: dict[str, Button] = {} - self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") - self.wifi_manager.set_callbacks(need_auth=self._on_need_auth, - activated=self._on_activated, - forgotten=self._on_forgotten, - networks_updated=self._on_network_updated, - disconnected=self._on_disconnected) + self._wifi_manager.add_callbacks(need_auth=self._on_need_auth, + activated=self._on_activated, + forgotten=self._on_forgotten, + networks_updated=self._on_network_updated, + disconnected=self._on_disconnected) def show_event(self): # start/stop scanning when widget is visible - self.wifi_manager.set_active(True) + self._wifi_manager.set_active(True) def hide_event(self): - self.wifi_manager.set_active(False) + self._wifi_manager.set_active(False) def _load_icons(self): for icon in STRENGTH_ICONS + ["icons/checkmark.png", "icons/circled_slash.png", "icons/lock_closed.png"]: gui_app.texture(icon, ICON_SIZE, ICON_SIZE) - def _render(self, rect: rl.Rectangle): - self.wifi_manager.process_callbacks() + def _update_state(self): + self._wifi_manager.process_callbacks() + def _render(self, rect: rl.Rectangle): if not self._networks: - gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) return if self.state == UIState.NEEDS_AUTH and self._state_network: - self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") - self.keyboard.reset() + self.keyboard.set_title(tr("Wrong password") if self._password_retry else tr("Enter password"), tr("for \"{}\"").format(self._state_network.ssid)) + self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH) gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: - self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') - self._confirm_dialog.reset() - gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) + confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel")) + confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(self._state_network.ssid)) + confirm_dialog.reset() + gui_app.set_modal_overlay(confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) else: self._draw_network_list(rect) @@ -104,24 +340,23 @@ class WifiManagerUI(Widget): def _draw_network_list(self, rect: rl.Rectangle): content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT) - offset = self.scroll_panel.handle_scroll(rect, content_rect) - clicked = self.scroll_panel.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) + offset = self.scroll_panel.update(rect, content_rect) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) for i, network in enumerate(self._networks): - y_offset = rect.y + i * ITEM_HEIGHT + offset.y + y_offset = rect.y + i * ITEM_HEIGHT + offset item_rect = rl.Rectangle(rect.x, y_offset, rect.width, ITEM_HEIGHT) if not rl.check_collision_recs(item_rect, rect): continue - self._draw_network_item(item_rect, network, clicked) + self._draw_network_item(item_rect, network) if i < len(self._networks) - 1: line_y = int(item_rect.y + item_rect.height - 1) rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY) rl.end_scissor_mode() - def _draw_network_item(self, rect, network: Network, clicked: bool): + def _draw_network_item(self, rect, network: Network): spacing = 50 ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT) signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) @@ -131,11 +366,11 @@ class WifiManagerUI(Widget): if self.state == UIState.CONNECTING and self._state_network: if self._state_network.ssid == network.ssid: self._networks_buttons[network.ssid].set_enabled(False) - status_text = "CONNECTING..." + status_text = tr("CONNECTING...") elif self.state == UIState.FORGETTING and self._state_network: if self._state_network.ssid == network.ssid: self._networks_buttons[network.ssid].set_enabled(False) - status_text = "FORGETTING..." + status_text = tr("FORGETTING...") elif network.security_type == SecurityType.UNSUPPORTED: self._networks_buttons[network.ssid].set_enabled(False) else: @@ -161,18 +396,16 @@ class WifiManagerUI(Widget): self._draw_signal_strength_icon(signal_icon_rect, network) def _networks_buttons_callback(self, network): - if self.scroll_panel.is_touch_valid(): - if not network.is_saved and network.security_type != SecurityType.OPEN: - self.state = UIState.NEEDS_AUTH - self._state_network = network - self._password_retry = False - elif not network.is_connected: - self.connect_to_network(network) + if not network.is_saved and network.security_type != SecurityType.OPEN: + self.state = UIState.NEEDS_AUTH + self._state_network = network + self._password_retry = False + elif not network.is_connected: + self.connect_to_network(network) def _forget_networks_buttons_callback(self, network): - if self.scroll_panel.is_touch_valid(): - self.state = UIState.SHOW_FORGET_CONFIRM - self._state_network = network + self.state = UIState.SHOW_FORGET_CONFIRM + self._state_network = network def _draw_status_icon(self, rect, network: Network): """Draw the status icon based on network's connection state""" @@ -200,22 +433,24 @@ class WifiManagerUI(Widget): self.state = UIState.CONNECTING self._state_network = network if network.is_saved and not password: - self.wifi_manager.activate_connection(network.ssid) + self._wifi_manager.activate_connection(network.ssid) else: - self.wifi_manager.connect_to_network(network.ssid, password) + self._wifi_manager.connect_to_network(network.ssid, password) def forget_network(self, network: Network): self.state = UIState.FORGETTING self._state_network = network - self.wifi_manager.forget_connection(network.ssid) + self._wifi_manager.forget_connection(network.ssid) def _on_network_updated(self, networks: list[Network]): self._networks = networks for n in self._networks: - self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, - button_style=ButtonStyle.NO_EFFECT) - self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, + self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, + text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) + self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) + self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, font_size=45) + self._forget_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) def _on_need_auth(self, ssid): network = next((n for n in self._networks if n.ssid == ssid), None) diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index 3140f419f5..3b2201164a 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -1,9 +1,10 @@ import pyray as rl from openpilot.system.ui.lib.application import FontWeight -from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, TextAlignment +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.label import gui_label +from openpilot.system.ui.widgets.scroller import Scroller # Constants MARGIN = 50 @@ -16,13 +17,29 @@ LIST_ITEM_SPACING = 25 class MultiOptionDialog(Widget): - def __init__(self, title, options, current=""): + def __init__(self, title, options, current="", option_font_weight=FontWeight.MEDIUM): super().__init__() self.title = title self.options = options self.current = current self.selection = current - self.scroll = GuiScrollPanel() + self._result: DialogResult = DialogResult.NO_ACTION + + # Create scroller with option buttons + self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt), + font_weight=option_font_weight, + text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.NORMAL, + text_padding=50, elide_right=True) for option in options] + self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING) + + self.cancel_button = Button(lambda: tr("Cancel"), click_callback=lambda: self._set_result(DialogResult.CANCEL)) + self.select_button = Button(lambda: tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY) + + def _set_result(self, result: DialogResult): + self._result = result + + def _on_option_clicked(self, option): + self.selection = option def _render(self, rect): dialog_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - 2 * MARGIN, rect.height - 2 * MARGIN) @@ -36,36 +53,26 @@ class MultiOptionDialog(Widget): # Options area options_y = content_rect.y + TITLE_FONT_SIZE + ITEM_SPACING options_h = content_rect.height - TITLE_FONT_SIZE - BUTTON_HEIGHT - 2 * ITEM_SPACING - view_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, options_h) - content_h = len(self.options) * (ITEM_HEIGHT + 10) - list_content_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, content_h) + options_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, options_h) - # Scroll and render options - offset = self.scroll.handle_scroll(view_rect, list_content_rect) - valid_click = self.scroll.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) - - rl.begin_scissor_mode(int(view_rect.x), int(options_y), int(view_rect.width), int(options_h)) + # Update button styles and set width based on selection for i, option in enumerate(self.options): - item_y = options_y + i * (ITEM_HEIGHT + LIST_ITEM_SPACING) + offset.y - item_rect = rl.Rectangle(view_rect.x, item_y, view_rect.width, ITEM_HEIGHT) + selected = option == self.selection + button = self.option_buttons[i] + button.set_button_style(ButtonStyle.PRIMARY if selected else ButtonStyle.NORMAL) + button.set_rect(rl.Rectangle(0, 0, options_rect.width, ITEM_HEIGHT)) - if rl.check_collision_recs(item_rect, view_rect): - selected = option == self.selection - style = ButtonStyle.PRIMARY if selected else ButtonStyle.NORMAL - - if gui_button(item_rect, option, button_style=style, text_alignment=TextAlignment.LEFT) and valid_click: - self.selection = option - rl.end_scissor_mode() + self.scroller.render(options_rect) # Buttons button_y = content_rect.y + content_rect.height - BUTTON_HEIGHT button_w = (content_rect.width - BUTTON_SPACING) / 2 - if gui_button(rl.Rectangle(content_rect.x, button_y, button_w, BUTTON_HEIGHT), "Cancel"): - return 0 + cancel_rect = rl.Rectangle(content_rect.x, button_y, button_w, BUTTON_HEIGHT) + self.cancel_button.render(cancel_rect) - if gui_button(rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT), - "Select", is_enabled=self.selection != self.current, button_style=ButtonStyle.PRIMARY): - return 1 + select_rect = rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT) + self.select_button.set_enabled(self.selection != self.current) + self.select_button.render(select_rect) - return -1 + return self._result diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index f7304bf6a6..a843010d56 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -18,7 +18,7 @@ class LineSeparator(Widget): def _render(self, _): rl.draw_line(int(self._rect.x) + LINE_PADDING, int(self._rect.y), - int(self._rect.x + self._rect.width) - LINE_PADDING * 2, int(self._rect.y), + int(self._rect.x + self._rect.width) - LINE_PADDING, int(self._rect.y), LINE_COLOR) @@ -27,7 +27,7 @@ class Scroller(Widget): super().__init__() self._items: list[Widget] = [] self._spacing = spacing - self._line_separator = line_separator + self._line_separator = LineSeparator() if line_separator else None self._pad_end = pad_end self.scroll_panel = GuiScrollPanel() @@ -36,18 +36,23 @@ class Scroller(Widget): self.add_widget(item) def add_widget(self, item: Widget) -> None: - if self._line_separator and len(self._items) > 0: - self._items.append(LineSeparator()) self._items.append(item) item.set_touch_valid_callback(self.scroll_panel.is_touch_valid) def _render(self, _): # TODO: don't draw items that are not in the viewport visible_items = [item for item in self._items if item.is_visible] + + # Add line separator between items + if self._line_separator is not None: + l = len(visible_items) + for i in range(1, len(visible_items)): + visible_items.insert(l - i, self._line_separator) + content_height = sum(item.rect.height for item in visible_items) + self._spacing * (len(visible_items)) if not self._pad_end: content_height -= self._spacing - scroll = self.scroll_panel.handle_scroll(self._rect, rl.Rectangle(0, 0, self._rect.width, content_height)) + scroll = self.scroll_panel.update(self._rect, rl.Rectangle(0, 0, self._rect.width, content_height)) rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) @@ -63,8 +68,7 @@ class Scroller(Widget): cur_height += item.rect.height + self._spacing * (idx != 0) # Consider scroll - x += scroll.x - y += scroll.y + y += scroll # Update item state item.set_position(x, y) @@ -72,3 +76,15 @@ class Scroller(Widget): item.render() rl.end_scissor_mode() + + def show_event(self): + super().show_event() + # Reset to top + self.scroll_panel.set_offset(0) + for item in self._items: + item.show_event() + + def hide_event(self): + super().hide_event() + for item in self._items: + item.hide_event() diff --git a/system/ui/widgets/toggle.py b/system/ui/widgets/toggle.py index eba5ef9e58..0fbf3c844a 100644 --- a/system/ui/widgets/toggle.py +++ b/system/ui/widgets/toggle.py @@ -1,4 +1,5 @@ import pyray as rl +from collections.abc import Callable from openpilot.system.ui.lib.application import MousePos from openpilot.system.ui.widgets import Widget @@ -14,12 +15,14 @@ ANIMATION_SPEED = 8.0 class Toggle(Widget): - def __init__(self, initial_state=False): + def __init__(self, initial_state: bool = False, callback: Callable[[bool], None] | None = None): super().__init__() self._state = initial_state + self._callback = callback self._enabled = True self._progress = 1.0 if initial_state else 0.0 self._target = self._progress + self._clicked = False def set_rect(self, rect: rl.Rectangle): self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT) @@ -28,10 +31,13 @@ class Toggle(Widget): if not self._enabled: return + self._clicked = True self._state = not self._state self._target = 1.0 if self._state else 0.0 + if self._callback: + self._callback(self._state) - def get_state(self): + def get_state(self) -> bool: return self._state def set_state(self, state: bool): @@ -66,5 +72,10 @@ class Toggle(Widget): knob_y = self._rect.y + HEIGHT / 2 rl.draw_circle(int(knob_x), int(knob_y), HEIGHT / 2, knob_color) + # TODO: use click callback + clicked = self._clicked + self._clicked = False + return clicked + def _blend_color(self, c1, c2, t): return rl.Color(int(c1.r + (c2.r - c1.r) * t), int(c1.g + (c2.g - c1.g) * t), int(c1.b + (c2.b - c1.b) * t), 255) diff --git a/system/updated/updated.py b/system/updated/updated.py index 181e410fa6..e0c9e82dbb 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -370,6 +370,8 @@ class Updater: setup_git_options(OVERLAY_MERGED) + run(["git", "config", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"], OVERLAY_MERGED) + branch = self.target_branch git_fetch_output = run(["git", "fetch", "origin", branch], OVERLAY_MERGED) cloudlog.info("git fetch success: %s", git_fetch_output) @@ -377,6 +379,7 @@ class Updater: cloudlog.info("git reset in progress") cmds = [ ["git", "checkout", "--force", "--no-recurse-submodules", "-B", branch, "FETCH_HEAD"], + ["git", "branch", "--set-upstream-to", f"origin/{branch}"], ["git", "reset", "--hard"], ["git", "clean", "-xdff"], ["git", "submodule", "sync"], diff --git a/system/version.py b/system/version.py index a3f2e36da5..f3de2d1bf2 100755 --- a/system/version.py +++ b/system/version.py @@ -13,8 +13,8 @@ from openpilot.common.git import get_commit, get_origin, get_branch, get_short_b RELEASE_SP_BRANCHES = ['release-c3', 'release', 'release-tizi', 'release-tici', 'release-tizi-staging', 'release-tici-staging'] TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new', 'staging'] MASTER_SP_BRANCHES = ['master'] -RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] -TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + RELEASE_SP_BRANCHES + TESTED_SP_BRANCHES +RELEASE_BRANCHES = ['release-tizi-staging', 'release-tici', 'release-tizi', 'nightly'] +TESTED_BRANCHES = RELEASE_BRANCHES + ['devel-staging', 'nightly-dev'] + RELEASE_SP_BRANCHES + TESTED_SP_BRANCHES SP_BRANCH_MIGRATIONS = { ("tici", "staging-c3-new"): "staging-tici", @@ -50,9 +50,7 @@ def is_prebuilt(path: str = BASEDIR) -> bool: @cache def is_dirty(cwd: str = BASEDIR) -> bool: - origin = get_origin() - branch = get_branch() - if not origin or not branch: + if not get_origin() or not get_short_branch(): return True dirty = False @@ -65,6 +63,9 @@ def is_dirty(cwd: str = BASEDIR) -> bool: except subprocess.CalledProcessError: pass + branch = get_branch() + if not branch: + return True dirty = (subprocess.call(["git", "diff-index", "--quiet", branch, "--"], cwd=cwd)) != 0 except subprocess.CalledProcessError: cloudlog.exception("git subprocess failed while checking dirty") diff --git a/system/webrtc/device/video.py b/system/webrtc/device/video.py index 1bca909294..50feab4f4a 100644 --- a/system/webrtc/device/video.py +++ b/system/webrtc/device/video.py @@ -1,4 +1,5 @@ import asyncio +import time import av from teleoprtc.tracks import TiciVideoStreamTrack @@ -20,6 +21,7 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): self._sock = messaging.sub_sock(self.camera_to_sock_mapping[camera_type], conflate=True) self._pts = 0 + self._t0_ns = time.monotonic_ns() async def recv(self): while True: @@ -32,10 +34,10 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): packet = av.Packet(evta.header + evta.data) packet.time_base = self._time_base - packet.pts = self._pts - self.log_debug("track sending frame %s", self._pts) - self._pts += self._dt * self._clock_rate + self._pts = ((time.monotonic_ns() - self._t0_ns) * self._clock_rate) // 1_000_000_000 + packet.pts = self._pts + self.log_debug("track sending frame %d", self._pts) return packet diff --git a/system/webrtc/tests/test_stream_session.py b/system/webrtc/tests/test_stream_session.py index 113fa5e7e6..e31fda3728 100644 --- a/system/webrtc/tests/test_stream_session.py +++ b/system/webrtc/tests/test_stream_session.py @@ -1,5 +1,6 @@ import asyncio import json +import time # for aiortc and its dependencies import warnings warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -14,7 +15,6 @@ from cereal import messaging, log from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack from openpilot.system.webrtc.device.audio import AudioInputStreamTrack -from openpilot.common.realtime import DT_DMON class TestStreamSession: @@ -81,7 +81,10 @@ class TestStreamSession: for i in range(5): packet = self.loop.run_until_complete(track.recv()) assert packet.time_base == VIDEO_TIME_BASE - assert packet.pts == int(i * DT_DMON * VIDEO_CLOCK_RATE) + if i == 0: + start_ns = time.monotonic_ns() + start_pts = packet.pts + assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms assert packet.size == 0 def test_input_audio_track(self, mocker): diff --git a/system/webrtc/webrtcd.py b/system/webrtc/webrtcd.py index fb93e565ff..c19f1bf9dd 100755 --- a/system/webrtc/webrtcd.py +++ b/system/webrtc/webrtcd.py @@ -239,6 +239,20 @@ async def get_schema(request: 'web.Request'): schema_dict = {s: generate_field(log.Event.schema.fields[s]) for s in services} return web.json_response(schema_dict) +async def post_notify(request: 'web.Request'): + try: + payload = await request.json() + except Exception as e: + raise web.HTTPBadRequest(text="Invalid JSON") from e + + for session in list(request.app.get('streams', {}).values()): + try: + ch = session.stream.get_messaging_channel() + ch.send(json.dumps(payload)) + except Exception: + continue + + return web.Response(status=200, text="OK") async def on_shutdown(app: 'web.Application'): for session in app['streams'].values(): @@ -258,6 +272,7 @@ def webrtcd_thread(host: str, port: int, debug: bool): app['debug'] = debug app.on_shutdown.append(on_shutdown) app.router.add_post("/stream", get_stream) + app.router.add_post("/notify", post_notify) app.router.add_get("/schema", get_schema) web.run_app(app, host=host, port=port) diff --git a/third_party/qrcode/QrCode.cc b/third_party/qrcode/QrCode.cc deleted file mode 100644 index b9de86215e..0000000000 --- a/third_party/qrcode/QrCode.cc +++ /dev/null @@ -1,862 +0,0 @@ -/* - * QR Code generator library (C++) - * - * Copyright (c) Project Nayuki. (MIT License) - * https://www.nayuki.io/page/qr-code-generator-library - * - * 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. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include "QrCode.hpp" - -using std::int8_t; -using std::uint8_t; -using std::size_t; -using std::vector; - - -namespace qrcodegen { - -QrSegment::Mode::Mode(int mode, int cc0, int cc1, int cc2) : - modeBits(mode) { - numBitsCharCount[0] = cc0; - numBitsCharCount[1] = cc1; - numBitsCharCount[2] = cc2; -} - - -int QrSegment::Mode::getModeBits() const { - return modeBits; -} - - -int QrSegment::Mode::numCharCountBits(int ver) const { - return numBitsCharCount[(ver + 7) / 17]; -} - - -const QrSegment::Mode QrSegment::Mode::NUMERIC (0x1, 10, 12, 14); -const QrSegment::Mode QrSegment::Mode::ALPHANUMERIC(0x2, 9, 11, 13); -const QrSegment::Mode QrSegment::Mode::BYTE (0x4, 8, 16, 16); -const QrSegment::Mode QrSegment::Mode::KANJI (0x8, 8, 10, 12); -const QrSegment::Mode QrSegment::Mode::ECI (0x7, 0, 0, 0); - - -QrSegment QrSegment::makeBytes(const vector &data) { - if (data.size() > static_cast(INT_MAX)) - throw std::length_error("Data too long"); - BitBuffer bb; - for (uint8_t b : data) - bb.appendBits(b, 8); - return QrSegment(Mode::BYTE, static_cast(data.size()), std::move(bb)); -} - - -QrSegment QrSegment::makeNumeric(const char *digits) { - BitBuffer bb; - int accumData = 0; - int accumCount = 0; - int charCount = 0; - for (; *digits != '\0'; digits++, charCount++) { - char c = *digits; - if (c < '0' || c > '9') - throw std::domain_error("String contains non-numeric characters"); - accumData = accumData * 10 + (c - '0'); - accumCount++; - if (accumCount == 3) { - bb.appendBits(static_cast(accumData), 10); - accumData = 0; - accumCount = 0; - } - } - if (accumCount > 0) // 1 or 2 digits remaining - bb.appendBits(static_cast(accumData), accumCount * 3 + 1); - return QrSegment(Mode::NUMERIC, charCount, std::move(bb)); -} - - -QrSegment QrSegment::makeAlphanumeric(const char *text) { - BitBuffer bb; - int accumData = 0; - int accumCount = 0; - int charCount = 0; - for (; *text != '\0'; text++, charCount++) { - const char *temp = std::strchr(ALPHANUMERIC_CHARSET, *text); - if (temp == nullptr) - throw std::domain_error("String contains unencodable characters in alphanumeric mode"); - accumData = accumData * 45 + static_cast(temp - ALPHANUMERIC_CHARSET); - accumCount++; - if (accumCount == 2) { - bb.appendBits(static_cast(accumData), 11); - accumData = 0; - accumCount = 0; - } - } - if (accumCount > 0) // 1 character remaining - bb.appendBits(static_cast(accumData), 6); - return QrSegment(Mode::ALPHANUMERIC, charCount, std::move(bb)); -} - - -vector QrSegment::makeSegments(const char *text) { - // Select the most efficient segment encoding automatically - vector result; - if (*text == '\0'); // Leave result empty - else if (isNumeric(text)) - result.push_back(makeNumeric(text)); - else if (isAlphanumeric(text)) - result.push_back(makeAlphanumeric(text)); - else { - vector bytes; - for (; *text != '\0'; text++) - bytes.push_back(static_cast(*text)); - result.push_back(makeBytes(bytes)); - } - return result; -} - - -QrSegment QrSegment::makeEci(long assignVal) { - BitBuffer bb; - if (assignVal < 0) - throw std::domain_error("ECI assignment value out of range"); - else if (assignVal < (1 << 7)) - bb.appendBits(static_cast(assignVal), 8); - else if (assignVal < (1 << 14)) { - bb.appendBits(2, 2); - bb.appendBits(static_cast(assignVal), 14); - } else if (assignVal < 1000000L) { - bb.appendBits(6, 3); - bb.appendBits(static_cast(assignVal), 21); - } else - throw std::domain_error("ECI assignment value out of range"); - return QrSegment(Mode::ECI, 0, std::move(bb)); -} - - -QrSegment::QrSegment(Mode md, int numCh, const std::vector &dt) : - mode(md), - numChars(numCh), - data(dt) { - if (numCh < 0) - throw std::domain_error("Invalid value"); -} - - -QrSegment::QrSegment(Mode md, int numCh, std::vector &&dt) : - mode(md), - numChars(numCh), - data(std::move(dt)) { - if (numCh < 0) - throw std::domain_error("Invalid value"); -} - - -int QrSegment::getTotalBits(const vector &segs, int version) { - int result = 0; - for (const QrSegment &seg : segs) { - int ccbits = seg.mode.numCharCountBits(version); - if (seg.numChars >= (1L << ccbits)) - return -1; // The segment's length doesn't fit the field's bit width - if (4 + ccbits > INT_MAX - result) - return -1; // The sum will overflow an int type - result += 4 + ccbits; - if (seg.data.size() > static_cast(INT_MAX - result)) - return -1; // The sum will overflow an int type - result += static_cast(seg.data.size()); - } - return result; -} - - -bool QrSegment::isAlphanumeric(const char *text) { - for (; *text != '\0'; text++) { - if (std::strchr(ALPHANUMERIC_CHARSET, *text) == nullptr) - return false; - } - return true; -} - - -bool QrSegment::isNumeric(const char *text) { - for (; *text != '\0'; text++) { - char c = *text; - if (c < '0' || c > '9') - return false; - } - return true; -} - - -QrSegment::Mode QrSegment::getMode() const { - return mode; -} - - -int QrSegment::getNumChars() const { - return numChars; -} - - -const std::vector &QrSegment::getData() const { - return data; -} - - -const char *QrSegment::ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; - - - -int QrCode::getFormatBits(Ecc ecl) { - switch (ecl) { - case Ecc::LOW : return 1; - case Ecc::MEDIUM : return 0; - case Ecc::QUARTILE: return 3; - case Ecc::HIGH : return 2; - default: throw std::logic_error("Assertion error"); - } -} - - -QrCode QrCode::encodeText(const char *text, Ecc ecl) { - vector segs = QrSegment::makeSegments(text); - return encodeSegments(segs, ecl); -} - - -QrCode QrCode::encodeBinary(const vector &data, Ecc ecl) { - vector segs{QrSegment::makeBytes(data)}; - return encodeSegments(segs, ecl); -} - - -QrCode QrCode::encodeSegments(const vector &segs, Ecc ecl, - int minVersion, int maxVersion, int mask, bool boostEcl) { - if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) - throw std::invalid_argument("Invalid value"); - - // Find the minimal version number to use - int version, dataUsedBits; - for (version = minVersion; ; version++) { - int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available - dataUsedBits = QrSegment::getTotalBits(segs, version); - if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) - break; // This version number is found to be suitable - if (version >= maxVersion) { // All versions in the range could not fit the given data - std::ostringstream sb; - if (dataUsedBits == -1) - sb << "Segment too long"; - else { - sb << "Data length = " << dataUsedBits << " bits, "; - sb << "Max capacity = " << dataCapacityBits << " bits"; - } - throw data_too_long(sb.str()); - } - } - if (dataUsedBits == -1) - throw std::logic_error("Assertion error"); - - // Increase the error correction level while the data still fits in the current version number - for (Ecc newEcl : vector{Ecc::MEDIUM, Ecc::QUARTILE, Ecc::HIGH}) { // From low to high - if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) - ecl = newEcl; - } - - // Concatenate all segments to create the data bit string - BitBuffer bb; - for (const QrSegment &seg : segs) { - bb.appendBits(static_cast(seg.getMode().getModeBits()), 4); - bb.appendBits(static_cast(seg.getNumChars()), seg.getMode().numCharCountBits(version)); - bb.insert(bb.end(), seg.getData().begin(), seg.getData().end()); - } - if (bb.size() != static_cast(dataUsedBits)) - throw std::logic_error("Assertion error"); - - // Add terminator and pad up to a byte if applicable - size_t dataCapacityBits = static_cast(getNumDataCodewords(version, ecl)) * 8; - if (bb.size() > dataCapacityBits) - throw std::logic_error("Assertion error"); - bb.appendBits(0, std::min(4, static_cast(dataCapacityBits - bb.size()))); - bb.appendBits(0, (8 - static_cast(bb.size() % 8)) % 8); - if (bb.size() % 8 != 0) - throw std::logic_error("Assertion error"); - - // Pad with alternating bytes until data capacity is reached - for (uint8_t padByte = 0xEC; bb.size() < dataCapacityBits; padByte ^= 0xEC ^ 0x11) - bb.appendBits(padByte, 8); - - // Pack bits into bytes in big endian - vector dataCodewords(bb.size() / 8); - for (size_t i = 0; i < bb.size(); i++) - dataCodewords[i >> 3] |= (bb.at(i) ? 1 : 0) << (7 - (i & 7)); - - // Create the QR Code object - return QrCode(version, ecl, dataCodewords, mask); -} - - -QrCode::QrCode(int ver, Ecc ecl, const vector &dataCodewords, int msk) : - // Initialize fields and check arguments - version(ver), - errorCorrectionLevel(ecl) { - if (ver < MIN_VERSION || ver > MAX_VERSION) - throw std::domain_error("Version value out of range"); - if (msk < -1 || msk > 7) - throw std::domain_error("Mask value out of range"); - size = ver * 4 + 17; - size_t sz = static_cast(size); - modules = vector >(sz, vector(sz)); // Initially all white - isFunction = vector >(sz, vector(sz)); - - // Compute ECC, draw modules - drawFunctionPatterns(); - const vector allCodewords = addEccAndInterleave(dataCodewords); - drawCodewords(allCodewords); - - // Do masking - if (msk == -1) { // Automatically choose best mask - long minPenalty = LONG_MAX; - for (int i = 0; i < 8; i++) { - applyMask(i); - drawFormatBits(i); - long penalty = getPenaltyScore(); - if (penalty < minPenalty) { - msk = i; - minPenalty = penalty; - } - applyMask(i); // Undoes the mask due to XOR - } - } - if (msk < 0 || msk > 7) - throw std::logic_error("Assertion error"); - this->mask = msk; - applyMask(msk); // Apply the final choice of mask - drawFormatBits(msk); // Overwrite old format bits - - isFunction.clear(); - isFunction.shrink_to_fit(); -} - - -int QrCode::getVersion() const { - return version; -} - - -int QrCode::getSize() const { - return size; -} - - -QrCode::Ecc QrCode::getErrorCorrectionLevel() const { - return errorCorrectionLevel; -} - - -int QrCode::getMask() const { - return mask; -} - - -bool QrCode::getModule(int x, int y) const { - return 0 <= x && x < size && 0 <= y && y < size && module(x, y); -} - - -std::string QrCode::toSvgString(int border) const { - if (border < 0) - throw std::domain_error("Border must be non-negative"); - if (border > INT_MAX / 2 || border * 2 > INT_MAX - size) - throw std::overflow_error("Border too large"); - - std::ostringstream sb; - sb << "\n"; - sb << "\n"; - sb << "\n"; - sb << "\t\n"; - sb << "\t\n"; - sb << "\n"; - return sb.str(); -} - - -void QrCode::drawFunctionPatterns() { - // Draw horizontal and vertical timing patterns - for (int i = 0; i < size; i++) { - setFunctionModule(6, i, i % 2 == 0); - setFunctionModule(i, 6, i % 2 == 0); - } - - // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) - drawFinderPattern(3, 3); - drawFinderPattern(size - 4, 3); - drawFinderPattern(3, size - 4); - - // Draw numerous alignment patterns - const vector alignPatPos = getAlignmentPatternPositions(); - size_t numAlign = alignPatPos.size(); - for (size_t i = 0; i < numAlign; i++) { - for (size_t j = 0; j < numAlign; j++) { - // Don't draw on the three finder corners - if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))) - drawAlignmentPattern(alignPatPos.at(i), alignPatPos.at(j)); - } - } - - // Draw configuration data - drawFormatBits(0); // Dummy mask value; overwritten later in the constructor - drawVersion(); -} - - -void QrCode::drawFormatBits(int msk) { - // Calculate error correction code and pack bits - int data = getFormatBits(errorCorrectionLevel) << 3 | msk; // errCorrLvl is uint2, msk is uint3 - int rem = data; - for (int i = 0; i < 10; i++) - rem = (rem << 1) ^ ((rem >> 9) * 0x537); - int bits = (data << 10 | rem) ^ 0x5412; // uint15 - if (bits >> 15 != 0) - throw std::logic_error("Assertion error"); - - // Draw first copy - for (int i = 0; i <= 5; i++) - setFunctionModule(8, i, getBit(bits, i)); - setFunctionModule(8, 7, getBit(bits, 6)); - setFunctionModule(8, 8, getBit(bits, 7)); - setFunctionModule(7, 8, getBit(bits, 8)); - for (int i = 9; i < 15; i++) - setFunctionModule(14 - i, 8, getBit(bits, i)); - - // Draw second copy - for (int i = 0; i < 8; i++) - setFunctionModule(size - 1 - i, 8, getBit(bits, i)); - for (int i = 8; i < 15; i++) - setFunctionModule(8, size - 15 + i, getBit(bits, i)); - setFunctionModule(8, size - 8, true); // Always black -} - - -void QrCode::drawVersion() { - if (version < 7) - return; - - // Calculate error correction code and pack bits - int rem = version; // version is uint6, in the range [7, 40] - for (int i = 0; i < 12; i++) - rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); - long bits = static_cast(version) << 12 | rem; // uint18 - if (bits >> 18 != 0) - throw std::logic_error("Assertion error"); - - // Draw two copies - for (int i = 0; i < 18; i++) { - bool bit = getBit(bits, i); - int a = size - 11 + i % 3; - int b = i / 3; - setFunctionModule(a, b, bit); - setFunctionModule(b, a, bit); - } -} - - -void QrCode::drawFinderPattern(int x, int y) { - for (int dy = -4; dy <= 4; dy++) { - for (int dx = -4; dx <= 4; dx++) { - int dist = std::max(std::abs(dx), std::abs(dy)); // Chebyshev/infinity norm - int xx = x + dx, yy = y + dy; - if (0 <= xx && xx < size && 0 <= yy && yy < size) - setFunctionModule(xx, yy, dist != 2 && dist != 4); - } - } -} - - -void QrCode::drawAlignmentPattern(int x, int y) { - for (int dy = -2; dy <= 2; dy++) { - for (int dx = -2; dx <= 2; dx++) - setFunctionModule(x + dx, y + dy, std::max(std::abs(dx), std::abs(dy)) != 1); - } -} - - -void QrCode::setFunctionModule(int x, int y, bool isBlack) { - size_t ux = static_cast(x); - size_t uy = static_cast(y); - modules .at(uy).at(ux) = isBlack; - isFunction.at(uy).at(ux) = true; -} - - -bool QrCode::module(int x, int y) const { - return modules.at(static_cast(y)).at(static_cast(x)); -} - - -vector QrCode::addEccAndInterleave(const vector &data) const { - if (data.size() != static_cast(getNumDataCodewords(version, errorCorrectionLevel))) - throw std::invalid_argument("Invalid argument"); - - // Calculate parameter numbers - int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[static_cast(errorCorrectionLevel)][version]; - int blockEccLen = ECC_CODEWORDS_PER_BLOCK [static_cast(errorCorrectionLevel)][version]; - int rawCodewords = getNumRawDataModules(version) / 8; - int numShortBlocks = numBlocks - rawCodewords % numBlocks; - int shortBlockLen = rawCodewords / numBlocks; - - // Split data into blocks and append ECC to each block - vector > blocks; - const vector rsDiv = reedSolomonComputeDivisor(blockEccLen); - for (int i = 0, k = 0; i < numBlocks; i++) { - vector dat(data.cbegin() + k, data.cbegin() + (k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1))); - k += static_cast(dat.size()); - const vector ecc = reedSolomonComputeRemainder(dat, rsDiv); - if (i < numShortBlocks) - dat.push_back(0); - dat.insert(dat.end(), ecc.cbegin(), ecc.cend()); - blocks.push_back(std::move(dat)); - } - - // Interleave (not concatenate) the bytes from every block into a single sequence - vector result; - for (size_t i = 0; i < blocks.at(0).size(); i++) { - for (size_t j = 0; j < blocks.size(); j++) { - // Skip the padding byte in short blocks - if (i != static_cast(shortBlockLen - blockEccLen) || j >= static_cast(numShortBlocks)) - result.push_back(blocks.at(j).at(i)); - } - } - if (result.size() != static_cast(rawCodewords)) - throw std::logic_error("Assertion error"); - return result; -} - - -void QrCode::drawCodewords(const vector &data) { - if (data.size() != static_cast(getNumRawDataModules(version) / 8)) - throw std::invalid_argument("Invalid argument"); - - size_t i = 0; // Bit index into the data - // Do the funny zigzag scan - for (int right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair - if (right == 6) - right = 5; - for (int vert = 0; vert < size; vert++) { // Vertical counter - for (int j = 0; j < 2; j++) { - size_t x = static_cast(right - j); // Actual x coordinate - bool upward = ((right + 1) & 2) == 0; - size_t y = static_cast(upward ? size - 1 - vert : vert); // Actual y coordinate - if (!isFunction.at(y).at(x) && i < data.size() * 8) { - modules.at(y).at(x) = getBit(data.at(i >> 3), 7 - static_cast(i & 7)); - i++; - } - // If this QR Code has any remainder bits (0 to 7), they were assigned as - // 0/false/white by the constructor and are left unchanged by this method - } - } - } - if (i != data.size() * 8) - throw std::logic_error("Assertion error"); -} - - -void QrCode::applyMask(int msk) { - if (msk < 0 || msk > 7) - throw std::domain_error("Mask value out of range"); - size_t sz = static_cast(size); - for (size_t y = 0; y < sz; y++) { - for (size_t x = 0; x < sz; x++) { - bool invert; - switch (msk) { - case 0: invert = (x + y) % 2 == 0; break; - case 1: invert = y % 2 == 0; break; - case 2: invert = x % 3 == 0; break; - case 3: invert = (x + y) % 3 == 0; break; - case 4: invert = (x / 3 + y / 2) % 2 == 0; break; - case 5: invert = x * y % 2 + x * y % 3 == 0; break; - case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; - case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; - default: throw std::logic_error("Assertion error"); - } - modules.at(y).at(x) = modules.at(y).at(x) ^ (invert & !isFunction.at(y).at(x)); - } - } -} - - -long QrCode::getPenaltyScore() const { - long result = 0; - - // Adjacent modules in row having same color, and finder-like patterns - for (int y = 0; y < size; y++) { - bool runColor = false; - int runX = 0; - std::array runHistory = {}; - for (int x = 0; x < size; x++) { - if (module(x, y) == runColor) { - runX++; - if (runX == 5) - result += PENALTY_N1; - else if (runX > 5) - result++; - } else { - finderPenaltyAddHistory(runX, runHistory); - if (!runColor) - result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; - runColor = module(x, y); - runX = 1; - } - } - result += finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3; - } - // Adjacent modules in column having same color, and finder-like patterns - for (int x = 0; x < size; x++) { - bool runColor = false; - int runY = 0; - std::array runHistory = {}; - for (int y = 0; y < size; y++) { - if (module(x, y) == runColor) { - runY++; - if (runY == 5) - result += PENALTY_N1; - else if (runY > 5) - result++; - } else { - finderPenaltyAddHistory(runY, runHistory); - if (!runColor) - result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; - runColor = module(x, y); - runY = 1; - } - } - result += finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3; - } - - // 2*2 blocks of modules having same color - for (int y = 0; y < size - 1; y++) { - for (int x = 0; x < size - 1; x++) { - bool color = module(x, y); - if ( color == module(x + 1, y) && - color == module(x, y + 1) && - color == module(x + 1, y + 1)) - result += PENALTY_N2; - } - } - - // Balance of black and white modules - int black = 0; - for (const vector &row : modules) { - for (bool color : row) { - if (color) - black++; - } - } - int total = size * size; // Note that size is odd, so black/total != 1/2 - // Compute the smallest integer k >= 0 such that (45-5k)% <= black/total <= (55+5k)% - int k = static_cast((std::abs(black * 20L - total * 10L) + total - 1) / total) - 1; - result += k * PENALTY_N4; - return result; -} - - -vector QrCode::getAlignmentPatternPositions() const { - if (version == 1) - return vector(); - else { - int numAlign = version / 7 + 2; - int step = (version == 32) ? 26 : - (version*4 + numAlign*2 + 1) / (numAlign*2 - 2) * 2; - vector result; - for (int i = 0, pos = size - 7; i < numAlign - 1; i++, pos -= step) - result.insert(result.begin(), pos); - result.insert(result.begin(), 6); - return result; - } -} - - -int QrCode::getNumRawDataModules(int ver) { - if (ver < MIN_VERSION || ver > MAX_VERSION) - throw std::domain_error("Version number out of range"); - int result = (16 * ver + 128) * ver + 64; - if (ver >= 2) { - int numAlign = ver / 7 + 2; - result -= (25 * numAlign - 10) * numAlign - 55; - if (ver >= 7) - result -= 36; - } - if (!(208 <= result && result <= 29648)) - throw std::logic_error("Assertion error"); - return result; -} - - -int QrCode::getNumDataCodewords(int ver, Ecc ecl) { - return getNumRawDataModules(ver) / 8 - - ECC_CODEWORDS_PER_BLOCK [static_cast(ecl)][ver] - * NUM_ERROR_CORRECTION_BLOCKS[static_cast(ecl)][ver]; -} - - -vector QrCode::reedSolomonComputeDivisor(int degree) { - if (degree < 1 || degree > 255) - throw std::domain_error("Degree out of range"); - // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. - // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}. - vector result(static_cast(degree)); - result.at(result.size() - 1) = 1; // Start off with the monomial x^0 - - // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), - // and drop the highest monomial term which is always 1x^degree. - // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). - uint8_t root = 1; - for (int i = 0; i < degree; i++) { - // Multiply the current product by (x - r^i) - for (size_t j = 0; j < result.size(); j++) { - result.at(j) = reedSolomonMultiply(result.at(j), root); - if (j + 1 < result.size()) - result.at(j) ^= result.at(j + 1); - } - root = reedSolomonMultiply(root, 0x02); - } - return result; -} - - -vector QrCode::reedSolomonComputeRemainder(const vector &data, const vector &divisor) { - vector result(divisor.size()); - for (uint8_t b : data) { // Polynomial division - uint8_t factor = b ^ result.at(0); - result.erase(result.begin()); - result.push_back(0); - for (size_t i = 0; i < result.size(); i++) - result.at(i) ^= reedSolomonMultiply(divisor.at(i), factor); - } - return result; -} - - -uint8_t QrCode::reedSolomonMultiply(uint8_t x, uint8_t y) { - // Russian peasant multiplication - int z = 0; - for (int i = 7; i >= 0; i--) { - z = (z << 1) ^ ((z >> 7) * 0x11D); - z ^= ((y >> i) & 1) * x; - } - if (z >> 8 != 0) - throw std::logic_error("Assertion error"); - return static_cast(z); -} - - -int QrCode::finderPenaltyCountPatterns(const std::array &runHistory) const { - int n = runHistory.at(1); - if (n > size * 3) - throw std::logic_error("Assertion error"); - bool core = n > 0 && runHistory.at(2) == n && runHistory.at(3) == n * 3 && runHistory.at(4) == n && runHistory.at(5) == n; - return (core && runHistory.at(0) >= n * 4 && runHistory.at(6) >= n ? 1 : 0) - + (core && runHistory.at(6) >= n * 4 && runHistory.at(0) >= n ? 1 : 0); -} - - -int QrCode::finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const { - if (currentRunColor) { // Terminate black run - finderPenaltyAddHistory(currentRunLength, runHistory); - currentRunLength = 0; - } - currentRunLength += size; // Add white border to final run - finderPenaltyAddHistory(currentRunLength, runHistory); - return finderPenaltyCountPatterns(runHistory); -} - - -void QrCode::finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const { - if (runHistory.at(0) == 0) - currentRunLength += size; // Add white border to initial run - std::copy_backward(runHistory.cbegin(), runHistory.cend() - 1, runHistory.end()); - runHistory.at(0) = currentRunLength; -} - - -bool QrCode::getBit(long x, int i) { - return ((x >> i) & 1) != 0; -} - - -/*---- Tables of constants ----*/ - -const int QrCode::PENALTY_N1 = 3; -const int QrCode::PENALTY_N2 = 3; -const int QrCode::PENALTY_N3 = 40; -const int QrCode::PENALTY_N4 = 10; - - -const int8_t QrCode::ECC_CODEWORDS_PER_BLOCK[4][41] = { - // Version: (note that index 0 is for padding, and is set to an illegal value) - //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level - {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low - {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium - {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile - {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High -}; - -const int8_t QrCode::NUM_ERROR_CORRECTION_BLOCKS[4][41] = { - // Version: (note that index 0 is for padding, and is set to an illegal value) - //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level - {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low - {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium - {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile - {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High -}; - - -data_too_long::data_too_long(const std::string &msg) : - std::length_error(msg) {} - - - -BitBuffer::BitBuffer() - : std::vector() {} - - -void BitBuffer::appendBits(std::uint32_t val, int len) { - if (len < 0 || len > 31 || val >> len != 0) - throw std::domain_error("Value out of range"); - for (int i = len - 1; i >= 0; i--) // Append bit by bit - this->push_back(((val >> i) & 1) != 0); -} - -} diff --git a/third_party/qrcode/QrCode.hpp b/third_party/qrcode/QrCode.hpp deleted file mode 100644 index 7341e41029..0000000000 --- a/third_party/qrcode/QrCode.hpp +++ /dev/null @@ -1,556 +0,0 @@ -/* - * QR Code generator library (C++) - * - * Copyright (c) Project Nayuki. (MIT License) - * https://www.nayuki.io/page/qr-code-generator-library - * - * 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. - */ - -#pragma once - -#include -#include -#include -#include -#include - - -namespace qrcodegen { - -/* - * A segment of character/binary/control data in a QR Code symbol. - * Instances of this class are immutable. - * The mid-level way to create a segment is to take the payload data - * and call a static factory function such as QrSegment::makeNumeric(). - * The low-level way to create a segment is to custom-make the bit buffer - * and call the QrSegment() constructor with appropriate values. - * This segment class imposes no length restrictions, but QR Codes have restrictions. - * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. - * Any segment longer than this is meaningless for the purpose of generating QR Codes. - */ -class QrSegment final { - - /*---- Public helper enumeration ----*/ - - /* - * Describes how a segment's data bits are interpreted. Immutable. - */ - public: class Mode final { - - /*-- Constants --*/ - - public: static const Mode NUMERIC; - public: static const Mode ALPHANUMERIC; - public: static const Mode BYTE; - public: static const Mode KANJI; - public: static const Mode ECI; - - - /*-- Fields --*/ - - // The mode indicator bits, which is a uint4 value (range 0 to 15). - private: int modeBits; - - // Number of character count bits for three different version ranges. - private: int numBitsCharCount[3]; - - - /*-- Constructor --*/ - - private: Mode(int mode, int cc0, int cc1, int cc2); - - - /*-- Methods --*/ - - /* - * (Package-private) Returns the mode indicator bits, which is an unsigned 4-bit value (range 0 to 15). - */ - public: int getModeBits() const; - - /* - * (Package-private) Returns the bit width of the character count field for a segment in - * this mode in a QR Code at the given version number. The result is in the range [0, 16]. - */ - public: int numCharCountBits(int ver) const; - - }; - - - - /*---- Static factory functions (mid level) ----*/ - - /* - * Returns a segment representing the given binary data encoded in - * byte mode. All input byte vectors are acceptable. Any text string - * can be converted to UTF-8 bytes and encoded as a byte mode segment. - */ - public: static QrSegment makeBytes(const std::vector &data); - - - /* - * Returns a segment representing the given string of decimal digits encoded in numeric mode. - */ - public: static QrSegment makeNumeric(const char *digits); - - - /* - * Returns a segment representing the given text string encoded in alphanumeric mode. - * The characters allowed are: 0 to 9, A to Z (uppercase only), space, - * dollar, percent, asterisk, plus, hyphen, period, slash, colon. - */ - public: static QrSegment makeAlphanumeric(const char *text); - - - /* - * Returns a list of zero or more segments to represent the given text string. The result - * may use various segment modes and switch modes to optimize the length of the bit stream. - */ - public: static std::vector makeSegments(const char *text); - - - /* - * Returns a segment representing an Extended Channel Interpretation - * (ECI) designator with the given assignment value. - */ - public: static QrSegment makeEci(long assignVal); - - - /*---- Public static helper functions ----*/ - - /* - * Tests whether the given string can be encoded as a segment in alphanumeric mode. - * A string is encodable iff each character is in the following set: 0 to 9, A to Z - * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. - */ - public: static bool isAlphanumeric(const char *text); - - - /* - * Tests whether the given string can be encoded as a segment in numeric mode. - * A string is encodable iff each character is in the range 0 to 9. - */ - public: static bool isNumeric(const char *text); - - - - /*---- Instance fields ----*/ - - /* The mode indicator of this segment. Accessed through getMode(). */ - private: Mode mode; - - /* The length of this segment's unencoded data. Measured in characters for - * numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. - * Always zero or positive. Not the same as the data's bit length. - * Accessed through getNumChars(). */ - private: int numChars; - - /* The data bits of this segment. Accessed through getData(). */ - private: std::vector data; - - - /*---- Constructors (low level) ----*/ - - /* - * Creates a new QR Code segment with the given attributes and data. - * The character count (numCh) must agree with the mode and the bit buffer length, - * but the constraint isn't checked. The given bit buffer is copied and stored. - */ - public: QrSegment(Mode md, int numCh, const std::vector &dt); - - - /* - * Creates a new QR Code segment with the given parameters and data. - * The character count (numCh) must agree with the mode and the bit buffer length, - * but the constraint isn't checked. The given bit buffer is moved and stored. - */ - public: QrSegment(Mode md, int numCh, std::vector &&dt); - - - /*---- Methods ----*/ - - /* - * Returns the mode field of this segment. - */ - public: Mode getMode() const; - - - /* - * Returns the character count field of this segment. - */ - public: int getNumChars() const; - - - /* - * Returns the data bits of this segment. - */ - public: const std::vector &getData() const; - - - // (Package-private) Calculates the number of bits needed to encode the given segments at - // the given version. Returns a non-negative number if successful. Otherwise returns -1 if a - // segment has too many characters to fit its length field, or the total bits exceeds INT_MAX. - public: static int getTotalBits(const std::vector &segs, int version); - - - /*---- Private constant ----*/ - - /* The set of all legal characters in alphanumeric mode, where - * each character value maps to the index in the string. */ - private: static const char *ALPHANUMERIC_CHARSET; - -}; - - - -/* - * A QR Code symbol, which is a type of two-dimension barcode. - * Invented by Denso Wave and described in the ISO/IEC 18004 standard. - * Instances of this class represent an immutable square grid of black and white cells. - * The class provides static factory functions to create a QR Code from text or binary data. - * The class covers the QR Code Model 2 specification, supporting all versions (sizes) - * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. - * - * Ways to create a QR Code object: - * - High level: Take the payload data and call QrCode::encodeText() or QrCode::encodeBinary(). - * - Mid level: Custom-make the list of segments and call QrCode::encodeSegments(). - * - Low level: Custom-make the array of data codeword bytes (including - * segment headers and final padding, excluding error correction codewords), - * supply the appropriate version number, and call the QrCode() constructor. - * (Note that all ways require supplying the desired error correction level.) - */ -class QrCode final { - - /*---- Public helper enumeration ----*/ - - /* - * The error correction level in a QR Code symbol. - */ - public: enum class Ecc { - LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords - MEDIUM , // The QR Code can tolerate about 15% erroneous codewords - QUARTILE, // The QR Code can tolerate about 25% erroneous codewords - HIGH , // The QR Code can tolerate about 30% erroneous codewords - }; - - - // Returns a value in the range 0 to 3 (unsigned 2-bit integer). - private: static int getFormatBits(Ecc ecl); - - - - /*---- Static factory functions (high level) ----*/ - - /* - * Returns a QR Code representing the given Unicode text string at the given error correction level. - * As a conservative upper bound, this function is guaranteed to succeed for strings that have 2953 or fewer - * UTF-8 code units (not Unicode code points) if the low error correction level is used. The smallest possible - * QR Code version is automatically chosen for the output. The ECC level of the result may be higher than - * the ecl argument if it can be done without increasing the version. - */ - public: static QrCode encodeText(const char *text, Ecc ecl); - - - /* - * Returns a QR Code representing the given binary data at the given error correction level. - * This function always encodes using the binary segment mode, not any text mode. The maximum number of - * bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output. - * The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. - */ - public: static QrCode encodeBinary(const std::vector &data, Ecc ecl); - - - /*---- Static factory functions (mid level) ----*/ - - /* - * Returns a QR Code representing the given segments with the given encoding parameters. - * The smallest possible QR Code version within the given range is automatically - * chosen for the output. Iff boostEcl is true, then the ECC level of the result - * may be higher than the ecl argument if it can be done without increasing the - * version. The mask number is either between 0 to 7 (inclusive) to force that - * mask, or -1 to automatically choose an appropriate mask (which may be slow). - * This function allows the user to create a custom sequence of segments that switches - * between modes (such as alphanumeric and byte) to encode text in less space. - * This is a mid-level API; the high-level API is encodeText() and encodeBinary(). - */ - public: static QrCode encodeSegments(const std::vector &segs, Ecc ecl, - int minVersion=1, int maxVersion=40, int mask=-1, bool boostEcl=true); // All optional parameters - - - - /*---- Instance fields ----*/ - - // Immutable scalar parameters: - - /* The version number of this QR Code, which is between 1 and 40 (inclusive). - * This determines the size of this barcode. */ - private: int version; - - /* The width and height of this QR Code, measured in modules, between - * 21 and 177 (inclusive). This is equal to version * 4 + 17. */ - private: int size; - - /* The error correction level used in this QR Code. */ - private: Ecc errorCorrectionLevel; - - /* The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive). - * Even if a QR Code is created with automatic masking requested (mask = -1), - * the resulting object still has a mask value between 0 and 7. */ - private: int mask; - - // Private grids of modules/pixels, with dimensions of size*size: - - // The modules of this QR Code (false = white, true = black). - // Immutable after constructor finishes. Accessed through getModule(). - private: std::vector > modules; - - // Indicates function modules that are not subjected to masking. Discarded when constructor finishes. - private: std::vector > isFunction; - - - - /*---- Constructor (low level) ----*/ - - /* - * Creates a new QR Code with the given version number, - * error correction level, data codeword bytes, and mask number. - * This is a low-level API that most users should not use directly. - * A mid-level API is the encodeSegments() function. - */ - public: QrCode(int ver, Ecc ecl, const std::vector &dataCodewords, int msk); - - - - /*---- Public instance methods ----*/ - - /* - * Returns this QR Code's version, in the range [1, 40]. - */ - public: int getVersion() const; - - - /* - * Returns this QR Code's size, in the range [21, 177]. - */ - public: int getSize() const; - - - /* - * Returns this QR Code's error correction level. - */ - public: Ecc getErrorCorrectionLevel() const; - - - /* - * Returns this QR Code's mask, in the range [0, 7]. - */ - public: int getMask() const; - - - /* - * Returns the color of the module (pixel) at the given coordinates, which is false - * for white or true for black. The top left corner has the coordinates (x=0, y=0). - * If the given coordinates are out of bounds, then false (white) is returned. - */ - public: bool getModule(int x, int y) const; - - - /* - * Returns a string of SVG code for an image depicting this QR Code, with the given number - * of border modules. The string always uses Unix newlines (\n), regardless of the platform. - */ - public: std::string toSvgString(int border) const; - - - - /*---- Private helper methods for constructor: Drawing function modules ----*/ - - // Reads this object's version field, and draws and marks all function modules. - private: void drawFunctionPatterns(); - - - // Draws two copies of the format bits (with its own error correction code) - // based on the given mask and this object's error correction level field. - private: void drawFormatBits(int msk); - - - // Draws two copies of the version bits (with its own error correction code), - // based on this object's version field, iff 7 <= version <= 40. - private: void drawVersion(); - - - // Draws a 9*9 finder pattern including the border separator, - // with the center module at (x, y). Modules can be out of bounds. - private: void drawFinderPattern(int x, int y); - - - // Draws a 5*5 alignment pattern, with the center module - // at (x, y). All modules must be in bounds. - private: void drawAlignmentPattern(int x, int y); - - - // Sets the color of a module and marks it as a function module. - // Only used by the constructor. Coordinates must be in bounds. - private: void setFunctionModule(int x, int y, bool isBlack); - - - // Returns the color of the module at the given coordinates, which must be in range. - private: bool module(int x, int y) const; - - - /*---- Private helper methods for constructor: Codewords and masking ----*/ - - // Returns a new byte string representing the given data with the appropriate error correction - // codewords appended to it, based on this object's version and error correction level. - private: std::vector addEccAndInterleave(const std::vector &data) const; - - - // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire - // data area of this QR Code. Function modules need to be marked off before this is called. - private: void drawCodewords(const std::vector &data); - - - // XORs the codeword modules in this QR Code with the given mask pattern. - // The function modules must be marked and the codeword bits must be drawn - // before masking. Due to the arithmetic of XOR, calling applyMask() with - // the same mask value a second time will undo the mask. A final well-formed - // QR Code needs exactly one (not zero, two, etc.) mask applied. - private: void applyMask(int msk); - - - // Calculates and returns the penalty score based on state of this QR Code's current modules. - // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. - private: long getPenaltyScore() const; - - - - /*---- Private helper functions ----*/ - - // Returns an ascending list of positions of alignment patterns for this version number. - // Each position is in the range [0,177), and are used on both the x and y axes. - // This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. - private: std::vector getAlignmentPatternPositions() const; - - - // Returns the number of data bits that can be stored in a QR Code of the given version number, after - // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. - // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. - private: static int getNumRawDataModules(int ver); - - - // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any - // QR Code of the given version number and error correction level, with remainder bits discarded. - // This stateless pure function could be implemented as a (40*4)-cell lookup table. - private: static int getNumDataCodewords(int ver, Ecc ecl); - - - // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be - // implemented as a lookup table over all possible parameter values, instead of as an algorithm. - private: static std::vector reedSolomonComputeDivisor(int degree); - - - // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials. - private: static std::vector reedSolomonComputeRemainder(const std::vector &data, const std::vector &divisor); - - - // Returns the product of the two given field elements modulo GF(2^8/0x11D). - // All inputs are valid. This could be implemented as a 256*256 lookup table. - private: static std::uint8_t reedSolomonMultiply(std::uint8_t x, std::uint8_t y); - - - // Can only be called immediately after a white run is added, and - // returns either 0, 1, or 2. A helper function for getPenaltyScore(). - private: int finderPenaltyCountPatterns(const std::array &runHistory) const; - - - // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore(). - private: int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const; - - - // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore(). - private: void finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const; - - - // Returns true iff the i'th bit of x is set to 1. - private: static bool getBit(long x, int i); - - - /*---- Constants and tables ----*/ - - // The minimum version number supported in the QR Code Model 2 standard. - public: static constexpr int MIN_VERSION = 1; - - // The maximum version number supported in the QR Code Model 2 standard. - public: static constexpr int MAX_VERSION = 40; - - - // For use in getPenaltyScore(), when evaluating which mask is best. - private: static const int PENALTY_N1; - private: static const int PENALTY_N2; - private: static const int PENALTY_N3; - private: static const int PENALTY_N4; - - - private: static const std::int8_t ECC_CODEWORDS_PER_BLOCK[4][41]; - private: static const std::int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41]; - -}; - - - -/*---- Public exception class ----*/ - -/* - * Thrown when the supplied data does not fit any QR Code version. Ways to handle this exception include: - * - Decrease the error correction level if it was greater than Ecc::LOW. - * - If the encodeSegments() function was called with a maxVersion argument, then increase - * it if it was less than QrCode::MAX_VERSION. (This advice does not apply to the other - * factory functions because they search all versions up to QrCode::MAX_VERSION.) - * - Split the text data into better or optimal segments in order to reduce the number of bits required. - * - Change the text or binary data to be shorter. - * - Change the text to fit the character set of a particular segment mode (e.g. alphanumeric). - * - Propagate the error upward to the caller/user. - */ -class data_too_long : public std::length_error { - - public: explicit data_too_long(const std::string &msg); - -}; - - - -/* - * An appendable sequence of bits (0s and 1s). Mainly used by QrSegment. - */ -class BitBuffer final : public std::vector { - - /*---- Constructor ----*/ - - // Creates an empty bit buffer (length 0). - public: BitBuffer(); - - - - /*---- Method ----*/ - - // Appends the given number of low-order bits of the given value - // to this buffer. Requires 0 <= len <= 31 and val < 2^len. - public: void appendBits(std::uint32_t val, int len); - -}; - -} diff --git a/third_party/qt5/larch64/bin/lrelease b/third_party/qt5/larch64/bin/lrelease deleted file mode 100755 index 5891f85851..0000000000 --- a/third_party/qt5/larch64/bin/lrelease +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:56ddfc4ead01eaf43cba41e2095ec4f52309c53a60ba9e351d8dbd900151228f -size 546824 diff --git a/third_party/qt5/larch64/bin/lupdate b/third_party/qt5/larch64/bin/lupdate deleted file mode 100755 index 0055aae229..0000000000 --- a/third_party/qt5/larch64/bin/lupdate +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7af679aa708031f6d19baabbd48e92d23462c6ff287caccdcb5c6324168d985d -size 1095744 diff --git a/third_party/raylib/build.sh b/third_party/raylib/build.sh index 544feb1c59..7f2ce5951f 100755 --- a/third_party/raylib/build.sh +++ b/third_party/raylib/build.sh @@ -1,6 +1,17 @@ #!/usr/bin/env bash set -e +SUDO="" + +# Use sudo if not root +if [[ ! $(id -u) -eq 0 ]]; then + if [[ -z $(which sudo) ]]; then + echo "Please install sudo or run as root" + exit 1 + fi + SUDO="sudo" +fi + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" cd $DIR @@ -10,6 +21,13 @@ ARCHNAME=$(uname -m) if [ -f /TICI ]; then ARCHNAME="larch64" RAYLIB_PLATFORM="PLATFORM_COMMA" +elif [[ "$OSTYPE" == "linux"* ]]; then + # required dependencies on Linux PC + $SUDO apt install \ + libxcursor-dev \ + libxi-dev \ + libxinerama-dev \ + libxrandr-dev fi if [[ "$OSTYPE" == "darwin"* ]]; then @@ -30,7 +48,7 @@ fi cd raylib_repo -COMMIT=${1:-39e6d8b52db159ba2ab3214b46d89a8069e09394} +COMMIT=${1:-3425bd9d1fb292ede4d80f97a1f4f258f614cffc} git fetch origin $COMMIT git reset --hard $COMMIT git clean -xdff . @@ -57,7 +75,7 @@ if [ -f /TICI ]; then cd raylib_python_repo - BINDINGS_COMMIT="ef8141c7979d5fa630ef4108605fc221f07d8cb7" + BINDINGS_COMMIT="a0710d95af3c12fd7f4b639589be9a13dad93cb6" git fetch origin $BINDINGS_COMMIT git reset --hard $BINDINGS_COMMIT git clean -xdff . diff --git a/third_party/raylib/larch64/libraylib.a b/third_party/raylib/larch64/libraylib.a index e1c5c19307..4e810c8b7b 100644 --- a/third_party/raylib/larch64/libraylib.a +++ b/third_party/raylib/larch64/libraylib.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c3125236db11e7bebcc6ad5868444ed0605c6343f98b212d39267c092b3b481 -size 3140628 +oid sha256:91e9a07513e84f7b553da01b34b24e12fe7130131ef73ebdb3dac3b838db815b +size 2001860 diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 89be3cceb2..d4cfc67280 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -1,7 +1,60 @@ -Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal', 'widgets') +import subprocess +import os + +Import('env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal') + +qt_env = env.Clone() +qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"] + +qt_libs = [] +if arch == "Darwin": + brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() + qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5" + qt_dirs = [ + os.path.join(qt_env['QTDIR'], "include"), + ] + qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules] + qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")] + qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"] + qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin")) +else: + qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip() + qt_install_headers = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip() + + qt_env['QTDIR'] = qt_install_prefix + qt_dirs = [ + f"{qt_install_headers}", + ] + + qt_gui_path = os.path.join(qt_install_headers, "QtGui") + qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))] + qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else [] + qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules] + + qt_libs = [f"Qt5{m}" for m in qt_modules] + if arch == "larch64": + qt_libs += ["GLESv2", "wayland-client"] + qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath) + elif arch != "Darwin": + qt_libs += ["GL"] +qt_env['QT3DIR'] = qt_env['QTDIR'] +qt_env.Tool('qt3') + +qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"] +qt_flags = [ + "-D_REENTRANT", + "-DQT_NO_DEBUG", + "-DQT_WIDGETS_LIB", + "-DQT_GUI_LIB", + "-DQT_CORE_LIB", + "-DQT_MESSAGELOGCONTEXT", +] +qt_env['CXXFLAGS'] += qt_flags +qt_env['LIBPATH'] += ['#selfdrive/ui', ] +qt_env['LIBS'] = qt_libs base_frameworks = qt_env['FRAMEWORKS'] -base_libs = [common, messaging, cereal, visionipc, 'qt_util', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] +base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] if arch == "Darwin": base_frameworks.append('OpenCL') @@ -12,11 +65,11 @@ else: base_libs.append('Qt5Charts') base_libs.append('Qt5SerialBus') -qt_libs = ['qt_util'] + base_libs +qt_libs = base_libs cabana_env = qt_env.Clone() -cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs +cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] @@ -28,7 +81,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "asset cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', - 'utils/export.cc', 'utils/util.cc', + 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'utils/api.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 97f178b1f3..bc50afc03a 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -1,7 +1,6 @@ #include #include -#include "selfdrive/ui/qt/util.h" #include "tools/cabana/mainwin.h" #include "tools/cabana/streams/devicestream.h" #include "tools/cabana/streams/pandastream.h" diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 505b2b8053..bc2380e550 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -324,7 +324,8 @@ void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap * if (!can->liveStreaming()) { s.segment_tree.build(s.vals); } - s.series->replace(QVector::fromStdVector(series_type == SeriesType::StepLine ? s.step_vals : s.vals)); + const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; + s.series->replace(QVector(points.cbegin(), points.cend())); } } updateAxisY(); @@ -382,7 +383,7 @@ void ChartView::updateAxisY() { QFontMetrics fm(axis_y->labelsFont()); for (int i = 0; i < tick_count; i++) { qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1)); - max_label_width = std::max(max_label_width, fm.width(QString::number(value, 'f', n))); + max_label_width = std::max(max_label_width, fm.horizontalAdvance(QString::number(value, 'f', n))); } int title_spacing = unit.isEmpty() ? 0 : QFontMetrics(axis_y->titleFont()).size(Qt::TextSingleLine, unit).height(); @@ -838,7 +839,8 @@ void ChartView::setSeriesType(SeriesType type) { } for (auto &s : sigs) { s.series = createSeries(series_type, s.sig->color); - s.series->replace(QVector::fromStdVector(series_type == SeriesType::StepLine ? s.step_vals : s.vals)); + const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; + s.series->replace(QVector(points.cbegin(), points.cend())); } updateSeriesPoints(); updateTitle(); diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 2e213988bc..4eda46f37b 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "tools/cabana/commands.h" diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 47304fbdc5..6df164b442 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -6,11 +6,11 @@ #include #include -#include "selfdrive/ui/qt/widgets/controls.h" #include "tools/cabana/binaryview.h" #include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/historylog.h" #include "tools/cabana/signalview.h" +#include "tools/cabana/utils/elidedlabel.h" class EditMessageDialog : public QDialog { public: diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 8361befb53..d65fc5b760 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -122,7 +122,7 @@ void MainWindow::createActions() { auto undo_act = UndoStack::instance()->createUndoAction(this, tr("&Undo")); undo_act->setShortcuts(QKeySequence::Undo); edit_menu->addAction(undo_act); - auto redo_act = UndoStack::instance()->createRedoAction(this, tr("&Rndo")); + auto redo_act = UndoStack::instance()->createRedoAction(this, tr("&Redo")); redo_act->setShortcuts(QKeySequence::Redo); edit_menu->addAction(redo_act); edit_menu->addSeparator(); diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 35537d75e3..0a0c07a155 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -265,7 +265,7 @@ QSize SignalItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QMo text += item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value); spacing += (option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1) * 2; } - width = std::min(option.widget->size().width() / 3.0, option.fontMetrics.width(text) + spacing); + width = std::min(option.widget->size().width() / 3.0, option.fontMetrics.horizontalAdvance(text) + spacing); } return {width, option.fontMetrics.height() + option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2}; } @@ -308,7 +308,7 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op // multiplexer indicator if (item->sig->type != cabana::Signal::Type::Normal) { QString indicator = item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value); - QRect indicator_rect{rect.x(), rect.y(), option.fontMetrics.width(indicator), rect.height()}; + QRect indicator_rect{rect.x(), rect.y(), option.fontMetrics.horizontalAdvance(indicator), rect.height()}; painter->setBrush(Qt::gray); painter->setPen(Qt::NoPen); painter->drawRoundedRect(indicator_rect, 3, 3); @@ -342,13 +342,13 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->drawText(rect, Qt::AlignLeft | Qt::AlignTop, max); painter->drawText(rect, Qt::AlignLeft | Qt::AlignBottom, min); QFontMetrics fm(minmax_font); - value_adjust = std::max(fm.width(min), fm.width(max)) + 5; + value_adjust = std::max(fm.horizontalAdvance(min), fm.horizontalAdvance(max)) + 5; } else if (!item->sparkline.isEmpty() && item->sig->type == cabana::Signal::Type::Multiplexed) { // display freq of multiplexed signal painter->setFont(label_font); QString freq = QString("%1 hz").arg(item->sparkline.freq(), 0, 'g', 2); painter->drawText(rect.adjusted(5, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, freq); - value_adjust = QFontMetrics(label_font).width(freq) + 10; + value_adjust = QFontMetrics(label_font).horizontalAdvance(freq) + 10; } // signal value painter->setFont(option.font); @@ -622,13 +622,13 @@ void SignalView::updateState(const std::set *msgs) { double value = 0; if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) { item->sig_val = item->sig->formatValue(value); - max_value_width = std::max(max_value_width, fontMetrics().width(item->sig_val)); + max_value_width = std::max(max_value_width, fontMetrics().horizontalAdvance(item->sig_val)); } } auto [first_visible, last_visible] = visibleSignalRange(); if (first_visible.isValid() && last_visible.isValid()) { - const static int min_max_width = QFontMetrics(delegate->minmax_font).width("-000.00") + 5; + const static int min_max_width = QFontMetrics(delegate->minmax_font).horizontalAdvance("-000.00") + 5; int available_width = value_column_width - delegate->button_size.width(); int value_width = std::min(max_value_width + min_max_width, available_width / 2); QSize size(available_width - value_width, diff --git a/tools/cabana/streams/routes.h b/tools/cabana/streams/routes.h index ee50712212..045dc67220 100644 --- a/tools/cabana/streams/routes.h +++ b/tools/cabana/streams/routes.h @@ -2,8 +2,7 @@ #include #include - -#include "selfdrive/ui/qt/api.h" +#include "tools/cabana/utils/api.h" class RouteListWidget; class OneShotHttpRequest; diff --git a/selfdrive/ui/qt/api.cc b/tools/cabana/utils/api.cc similarity index 66% rename from selfdrive/ui/qt/api.cc rename to tools/cabana/utils/api.cc index 85a3144bd2..89379a8434 100644 --- a/selfdrive/ui/qt/api.cc +++ b/tools/cabana/utils/api.cc @@ -1,7 +1,7 @@ -#include "selfdrive/ui/qt/api.h" +#include "tools/cabana/utils/api.h" +#include #include -#include #include #include @@ -13,35 +13,64 @@ #include #include +#include "common/params.h" #include "common/util.h" #include "system/hardware/hw.h" -#include "selfdrive/ui/qt/util.h" +#include "tools/cabana/utils/util.h" + +QString getVersion() { + static QString version = QString::fromStdString(Params().get("Version")); + return version; +} + +QString getUserAgent() { + return "openpilot-" + getVersion(); +} + +std::optional getDongleId() { + std::string id = Params().get("DongleId"); + + if (!id.empty() && (id != "UnregisteredDevice")) { + return QString::fromStdString(id); + } else { + return {}; + } +} namespace CommaApi { -RSA *get_rsa_private_key() { - static std::unique_ptr rsa_private(nullptr, RSA_free); - if (!rsa_private) { +EVP_PKEY *get_private_key() { + static std::unique_ptr pkey(nullptr, EVP_PKEY_free); + if (!pkey) { FILE *fp = fopen(Path::rsa_file().c_str(), "rb"); if (!fp) { - qDebug() << "No RSA private key found, please run manager.py or registration.py"; + qDebug() << "No private key found, please run manager.py or registration.py"; return nullptr; } - rsa_private.reset(PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL)); + pkey.reset(PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr)); fclose(fp); } - return rsa_private.get(); + return pkey.get(); } QByteArray rsa_sign(const QByteArray &data) { - RSA *rsa_private = get_rsa_private_key(); - if (!rsa_private) return {}; + EVP_PKEY *pkey = get_private_key(); + if (!pkey) return {}; - QByteArray sig(RSA_size(rsa_private), Qt::Uninitialized); - unsigned int sig_len; - int ret = RSA_sign(NID_sha256, (unsigned char*)data.data(), data.size(), (unsigned char*)sig.data(), &sig_len, rsa_private); - assert(ret == 1); - assert(sig.size() == sig_len); + EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); + if (!mdctx) return {}; + + QByteArray sig(EVP_PKEY_size(pkey), Qt::Uninitialized); + size_t sig_len = sig.size(); + + int ret = EVP_DigestSignInit(mdctx, nullptr, EVP_sha256(), nullptr, pkey); + ret &= EVP_DigestSignUpdate(mdctx, data.data(), data.size()); + ret &= EVP_DigestSignFinal(mdctx, (unsigned char*)sig.data(), &sig_len); + + EVP_MD_CTX_free(mdctx); + + if (ret != 1) return {}; + sig.resize(sig_len); return sig; } @@ -79,36 +108,31 @@ bool HttpRequest::timeout() const { return reply && reply->error() == QNetworkReply::OperationCanceledError; } -QNetworkRequest HttpRequest::prepareRequest(const QString &requestURL) { - QNetworkRequest request; +void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) { + if (active()) { + qDebug() << "HttpRequest is active"; + return; + } QString token; if (create_jwt) { - token = GetJwtToken(); + token = CommaApi::create_jwt(); } 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; -} -void HttpRequest::sendRequest(const QString &requestURL, const Method method) { - if (active()) { - qDebug() << "HttpRequest is active"; - return; - } - - QNetworkRequest request = prepareRequest(requestURL); - if (method == Method::GET) { + if (method == HttpRequest::Method::GET) { reply = nam()->get(request); - } else if (method == Method::DELETE) { + } else if (method == HttpRequest::Method::DELETE) { reply = nam()->deleteResource(request); } diff --git a/selfdrive/ui/qt/api.h b/tools/cabana/utils/api.h similarity index 66% rename from selfdrive/ui/qt/api.h rename to tools/cabana/utils/api.h index 80b85f7ca7..ad64d7e722 100644 --- a/selfdrive/ui/qt/api.h +++ b/tools/cabana/utils/api.h @@ -5,7 +5,6 @@ #include #include -#include "util.h" #include "common/util.h" namespace CommaApi { @@ -24,11 +23,10 @@ class HttpRequest : public QObject { Q_OBJECT public: - enum class Method {GET, DELETE, POST, PUT}; + enum class Method {GET, DELETE}; explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000); - virtual void sendRequest(const QString &requestURL, Method method); - void sendRequest(const QString &requestURL) { sendRequest(requestURL, Method::GET);} + void sendRequest(const QString &requestURL, const Method method = Method::GET); bool active() const; bool timeout() const; @@ -37,14 +35,13 @@ signals: 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(); } -protected slots: +private slots: void requestTimeout(); void requestFinished(); }; diff --git a/tools/cabana/utils/elidedlabel.cc b/tools/cabana/utils/elidedlabel.cc new file mode 100644 index 0000000000..629a108b16 --- /dev/null +++ b/tools/cabana/utils/elidedlabel.cc @@ -0,0 +1,29 @@ +#include "tools/cabana/utils/elidedlabel.h" +#include +#include + +ElidedLabel::ElidedLabel(QWidget *parent) : ElidedLabel({}, parent) {} + +ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + setMinimumWidth(1); +} + +void ElidedLabel::resizeEvent(QResizeEvent* event) { + QLabel::resizeEvent(event); + lastText_ = elidedText_ = ""; +} + +void ElidedLabel::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()); +} diff --git a/tools/cabana/utils/elidedlabel.h b/tools/cabana/utils/elidedlabel.h new file mode 100644 index 0000000000..577eea12a0 --- /dev/null +++ b/tools/cabana/utils/elidedlabel.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +class ElidedLabel : public QLabel { + Q_OBJECT + +public: + explicit ElidedLabel(QWidget *parent = 0); + explicit ElidedLabel(const QString &text, QWidget *parent = 0); + +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_; +}; diff --git a/tools/cabana/utils/util.cc b/tools/cabana/utils/util.cc index f27df4bf1e..ca069b358d 100644 --- a/tools/cabana/utils/util.cc +++ b/tools/cabana/utils/util.cc @@ -10,11 +10,16 @@ #include #include +#include #include #include #include - -#include "selfdrive/ui/qt/util.h" +#include +#include +#include +#include +#include +#include "common/util.h" // SegmentTree @@ -276,3 +281,80 @@ QString signalToolTip(const cabana::Signal *sig) { )").arg(sig->name).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb) .arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); } + +void setSurfaceFormat() { + QSurfaceFormat fmt; +#ifdef __APPLE__ + fmt.setVersion(3, 2); + fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + fmt.setRenderableType(QSurfaceFormat::OpenGL); +#else + fmt.setRenderableType(QSurfaceFormat::OpenGLES); +#endif + fmt.setSamples(16); + fmt.setStencilBufferSize(1); + QSurfaceFormat::setDefaultFormat(fmt); +} + +void sigTermHandler(int s) { + std::signal(s, SIG_DFL); + qApp->quit(); +} + +void initApp(int argc, char *argv[], bool disable_hidpi) { + // setup signal handlers to exit gracefully + std::signal(SIGINT, sigTermHandler); + std::signal(SIGTERM, sigTermHandler); + + QString app_dir; +#ifdef __APPLE__ + // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering + QApplication tmp(argc, argv); + app_dir = QCoreApplication::applicationDirPath(); + if (disable_hidpi) { + qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); + } +#else + app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path(); +#endif + + qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); + // ensure the current dir matches the exectuable's directory + QDir::setCurrent(app_dir); + + setSurfaceFormat(); +} + +static QHash load_bootstrap_icons() { + QHash icons; + + QFile f(":/bootstrap-icons.svg"); + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + QDomDocument xml; + xml.setContent(&f); + QDomNode n = xml.documentElement().firstChild(); + while (!n.isNull()) { + QDomElement e = n.toElement(); + if (!e.isNull() && e.hasAttribute("id")) { + QString svg_str; + QTextStream stream(&svg_str); + n.save(stream, 0); + svg_str.replace("", ""); + icons[e.attribute("id")] = svg_str.toUtf8(); + } + n = n.nextSibling(); + } + } + return icons; +} + +QPixmap bootstrapPixmap(const QString &id) { + static QHash icons = load_bootstrap_icons(); + + QPixmap pixmap; + if (auto it = icons.find(id); it != icons.end()) { + pixmap.loadFromData(it.value(), "svg"); + } + return pixmap; +} diff --git a/tools/cabana/utils/util.h b/tools/cabana/utils/util.h index 5c1cbe2d69..f839ffe7fe 100644 --- a/tools/cabana/utils/util.h +++ b/tools/cabana/utils/util.h @@ -166,3 +166,5 @@ private: 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'); } +void initApp(int argc, char *argv[], bool disable_hidpi = true); +QPixmap bootstrapPixmap(const QString &id); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index c857c9ffbe..55018f28f0 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -134,7 +134,7 @@ void VideoWidget::createSpeedDropdown(QToolBar *toolbar) { 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)); + speed_btn->setMinimumWidth(speed_btn->fontMetrics().horizontalAdvance("0.05x ") + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); } QWidget *VideoWidget::createCameraWidget() { diff --git a/tools/camerastream/README.md b/tools/camerastream/README.md index 7dbafb4be5..2f7498a07c 100644 --- a/tools/camerastream/README.md +++ b/tools/camerastream/README.md @@ -39,7 +39,7 @@ Decode the stream with `compressed_vipc.py`: To actually display the stream, run `watch3` in separate terminal: -```cd ~/openpilot/selfdrive/ui/ && ./watch3``` +```cd ~/openpilot/selfdrive/ui/ && ./watch3.py``` ## compressed_vipc.py usage ``` @@ -62,5 +62,5 @@ options: ## Example: ``` cd ~/openpilot/tools/camerastream && ./compressed_vipc.py comma-ffffffff --cams 0 -cd ~/openpilot/selfdrive/ui/ && ./watch3 +cd ~/openpilot/selfdrive/ui/ && ./watch3.py ``` diff --git a/tools/clip/run.py b/tools/clip/run.py index 8fa0e8eda3..9045a4381b 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -17,7 +17,7 @@ from cereal.messaging import SubMaster from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params, UnknownKeyName from openpilot.common.prefix import OpenpilotPrefix -from openpilot.common.run import managed_proc +from openpilot.common.utils import managed_proc from openpilot.tools.lib.route import Route from openpilot.tools.lib.logreader import LogReader diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh index f33569704a..5c2131d4bf 100755 --- a/tools/install_ubuntu_dependencies.sh +++ b/tools/install_ubuntu_dependencies.sh @@ -20,18 +20,25 @@ fi # Install common packages function install_ubuntu_common_requirements() { $SUDO apt-get update + + # normal stuff, mostly for the bare docker image $SUDO apt-get install -y --no-install-recommends \ ca-certificates \ clang \ build-essential \ - gcc-arm-none-eabi \ - liblzma-dev \ - capnproto \ - libcapnp-dev \ curl \ + libssl-dev \ libcurl4-openssl-dev \ + locales \ git \ git-lfs \ + xvfb + + # TODO: vendor the rest of these in third_party/ + $SUDO apt-get install -y --no-install-recommends \ + gcc-arm-none-eabi \ + capnproto \ + libcapnp-dev \ ffmpeg \ libavformat-dev \ libavcodec-dev \ @@ -41,20 +48,16 @@ function install_ubuntu_common_requirements() { libbz2-dev \ libeigen3-dev \ libffi-dev \ - libglew-dev \ libgles2-mesa-dev \ libglfw3-dev \ libglib2.0-0 \ libjpeg-dev \ libqt5charts5-dev \ libncurses5-dev \ - libssl-dev \ libusb-1.0-0-dev \ libzmq3-dev \ libzstd-dev \ libsqlite3-dev \ - libsystemd-dev \ - locales \ opencl-headers \ ocl-icd-libopencl1 \ ocl-icd-opencl-dev \ @@ -64,7 +67,7 @@ function install_ubuntu_common_requirements() { libqt5serialbus5-dev \ libqt5x11extras5-dev \ libqt5opengl5-dev \ - xvfb + gettext } # Install Ubuntu 24.04 LTS packages @@ -74,8 +77,6 @@ function install_ubuntu_lts_latest_requirements() { $SUDO apt-get install -y --no-install-recommends \ g++-12 \ qtbase5-dev \ - qtchooser \ - qt5-qmake \ qtbase5-dev-tools \ python3-dev \ python3-venv @@ -115,6 +116,11 @@ SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddcc", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddee", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddcc", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddee", MODE="0666" +EOF + + # Setup adb udev rules + $SUDO tee /etc/udev/rules.d/50-comma-adb.rules > /dev/null < dict: + return { + "tabs": { + str(tab_id): { + "name": tab_data["name"], + "panel_layout": tab_data["panel_layout"].to_dict() + } + for tab_id, tab_data in self.tabs.items() + } + } + + def clear_and_load_from_dict(self, data: dict): + tab_ids_to_close = list(self.tabs.keys()) + for tab_id in tab_ids_to_close: + self.close_tab(tab_id, force=True) + + for tab_id_str, tab_data in data["tabs"].items(): + tab_id = int(tab_id_str) + panel_layout = PanelLayoutManager.load_from_dict( + tab_data["panel_layout"], self.data_manager, self.playback_manager, + self.worker_manager, self.scale + ) + self.tabs[tab_id] = { + "name": tab_data["name"], + "panel_layout": panel_layout + } + + self.active_tab = min(self.tabs.keys()) if self.tabs else 0 + self._next_tab_id = max(self.tabs.keys()) + 1 if self.tabs else 1 + + def create_ui(self, parent_tag: str): + if dpg.does_item_exist(self.container_tag): + dpg.delete_item(self.container_tag) + + with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True): + self._create_tab_bar() + self._create_tab_content() + dpg.bind_item_theme(self.tab_bar_tag, "tab_bar_theme") + + def _create_tab_bar(self): + text_size = int(13 * self.scale) + with dpg.child_window(tag=self.tab_bar_tag, parent=self.container_tag, height=(text_size + 8), border=False, horizontal_scrollbar=True): + with dpg.group(horizontal=True, tag="tab_bar_group"): + for tab_id, tab_data in self.tabs.items(): + self._create_tab_ui(tab_id, tab_data["name"]) + dpg.add_image_button(texture_tag="plus_texture", callback=self.add_tab, width=text_size, height=text_size, tag="add_tab_button") + dpg.bind_item_theme("add_tab_button", "inactive_tab_theme") + + def _create_tab_ui(self, tab_id: int, tab_name: str): + text_size = int(13 * self.scale) + tab_width = int(140 * self.scale) + with dpg.child_window(width=tab_width, height=-1, border=False, no_scrollbar=True, tag=f"tab_window_{tab_id}", parent="tab_bar_group"): + with dpg.group(horizontal=True, tag=f"tab_group_{tab_id}"): + dpg.add_input_text( + default_value=tab_name, width=tab_width - text_size - 16, callback=lambda s, v, u: self.rename_tab(u, v), user_data=tab_id, tag=f"tab_input_{tab_id}" + ) + dpg.add_image_button( + texture_tag="x_texture", callback=lambda s, a, u: self.close_tab(u), user_data=tab_id, width=text_size, height=text_size, tag=f"tab_close_{tab_id}" + ) + with dpg.item_handler_registry(tag=f"tab_handler_{tab_id}"): + dpg.add_item_clicked_handler(callback=lambda s, a, u: self.switch_tab(u), user_data=tab_id) + dpg.bind_item_handler_registry(f"tab_group_{tab_id}", f"tab_handler_{tab_id}") + + theme_tag = "active_tab_theme" if tab_id == self.active_tab else "inactive_tab_theme" + dpg.bind_item_theme(f"tab_window_{tab_id}", theme_tag) + + def _create_tab_content(self): + with dpg.child_window(tag=self.tab_content_tag, parent=self.container_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True): + if self.active_tab in self.tabs: + active_panel_layout = self.tabs[self.active_tab]["panel_layout"] + active_panel_layout.create_ui() + + def add_tab(self): + new_panel_layout = PanelLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, self.scale) + new_tab = {"name": f"Tab {self._next_tab_id + 1}", "panel_layout": new_panel_layout} + self.tabs[self._next_tab_id] = new_tab + self._create_tab_ui(self._next_tab_id, new_tab["name"]) + dpg.move_item("add_tab_button", parent="tab_bar_group") # move plus button to end + self.switch_tab(self._next_tab_id) + self._next_tab_id += 1 + + def close_tab(self, tab_id: int, force = False): + if len(self.tabs) <= 1 and not force: + return # don't allow closing the last tab + + tab_to_close = self.tabs[tab_id] + tab_to_close["panel_layout"].destroy_ui() + for suffix in ["window", "group", "input", "close", "handler"]: + tag = f"tab_{suffix}_{tab_id}" + if dpg.does_item_exist(tag): + dpg.delete_item(tag) + del self.tabs[tab_id] + + if self.active_tab == tab_id and self.tabs: # switch to another tab if we closed the active one + self.active_tab = next(iter(self.tabs.keys())) + self._switch_tab_content() + dpg.bind_item_theme(f"tab_window_{self.active_tab}", "active_tab_theme") + + def switch_tab(self, tab_id: int): + if tab_id == self.active_tab or tab_id not in self.tabs: + return + + current_panel_layout = self.tabs[self.active_tab]["panel_layout"] + current_panel_layout.destroy_ui() + dpg.bind_item_theme(f"tab_window_{self.active_tab}", "inactive_tab_theme") # deactivate old tab + self.active_tab = tab_id + dpg.bind_item_theme(f"tab_window_{tab_id}", "active_tab_theme") # activate new tab + self._switch_tab_content() + + def _switch_tab_content(self): + dpg.delete_item(self.tab_content_tag, children_only=True) + active_panel_layout = self.tabs[self.active_tab]["panel_layout"] + active_panel_layout.create_ui() + active_panel_layout.update_all_panels() + + def rename_tab(self, tab_id: int, new_name: str): + if tab_id in self.tabs: + self.tabs[tab_id]["name"] = new_name + + def update_all_panels(self): + self.tabs[self.active_tab]["panel_layout"].update_all_panels() + + def on_viewport_resize(self): + self.tabs[self.active_tab]["panel_layout"].on_viewport_resize() + +class PanelLayoutManager: + def __init__(self, data_manager: DataManager, playback_manager, worker_manager, scale: float = 1.0): + self.data_manager = data_manager + self.playback_manager = playback_manager + self.worker_manager = worker_manager + self.scale = scale self.active_panels: list = [] + self.parent_tag = "tab_content_area" + self._queue_resize = False + self._created_handler_tags: set[str] = set() self.grip_size = int(GRIP_SIZE * self.scale) self.min_pane_size = int(MIN_PANE_SIZE * self.scale) @@ -21,13 +162,69 @@ class PlotLayoutManager: initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager) self.layout: dict = {"type": "panel", "panel": initial_panel} - def create_ui(self, parent_tag: str): - if dpg.does_item_exist(self.container_tag): - dpg.delete_item(self.container_tag) + def to_dict(self) -> dict: + return self._layout_to_dict(self.layout) - with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True): - container_width, container_height = dpg.get_item_rect_size(self.container_tag) - self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height) + def _layout_to_dict(self, layout: dict) -> dict: + if layout["type"] == "panel": + return { + "type": "panel", + "panel": layout["panel"].to_dict() + } + else: # split + return { + "type": "split", + "orientation": layout["orientation"], + "proportions": layout["proportions"], + "children": [self._layout_to_dict(child) for child in layout["children"]] + } + + @classmethod + def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager, scale: float = 1.0): + manager = cls(data_manager, playback_manager, worker_manager, scale) + manager.layout = manager._dict_to_layout(data) + return manager + + def _dict_to_layout(self, data: dict) -> dict: + if data["type"] == "panel": + panel_data = data["panel"] + if panel_data["type"] == "timeseries": + panel = TimeSeriesPanel.load_from_dict( + panel_data, self.data_manager, self.playback_manager, self.worker_manager + ) + return {"type": "panel", "panel": panel} + else: + # Handle future panel types here or make a general mapping + raise ValueError(f"Unknown panel type: {panel_data['type']}") + else: # split + return { + "type": "split", + "orientation": data["orientation"], + "proportions": data["proportions"], + "children": [self._dict_to_layout(child) for child in data["children"]] + } + + def create_ui(self): + self.active_panels.clear() + if dpg.does_item_exist(self.parent_tag): + dpg.delete_item(self.parent_tag, children_only=True) + self._cleanup_all_handlers() + + container_width, container_height = dpg.get_item_rect_size(self.parent_tag) + if container_width == 0 and container_height == 0: + self._queue_resize = True + self._create_ui_recursive(self.layout, self.parent_tag, [], container_width, container_height) + + def destroy_ui(self): + self._cleanup_ui_recursive(self.layout, []) + self._cleanup_all_handlers() + self.active_panels.clear() + + def _cleanup_all_handlers(self): + for handler_tag in list(self._created_handler_tags): + if dpg.does_item_exist(handler_tag): + dpg.delete_item(handler_tag) + self._created_handler_tags.clear() def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): if layout["type"] == "panel": @@ -35,18 +232,19 @@ class PlotLayoutManager: else: self._create_split_ui(layout, parent_tag, path, width, height) - def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height:int): + def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): panel_tag = self._path_to_tag(path, "panel") panel = layout["panel"] self.active_panels.append(panel) text_size = int(13 * self.scale) - bar_height = (text_size+24) if width < int(279 * self.scale + 80) else (text_size+8) # adjust height to allow for scrollbar + bar_height = (text_size + 24) if width < int(329 * self.scale + 64) else (text_size + 8) # adjust height to allow for scrollbar - with dpg.child_window(parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): + with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True): with dpg.group(horizontal=True): with dpg.child_window(tag=panel_tag, width=-(text_size + 16), height=bar_height, horizontal_scrollbar=True, no_scroll_with_mouse=True, border=False): with dpg.group(horizontal=True): - dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(panel, "title", v)) + # if you change the widths make sure to change the sum of widths (currently 329 * scale) + dpg.add_input_text(default_value=panel.title, width=int(150 * self.scale), callback=lambda s, v: setattr(panel, "title", v)) dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale)) dpg.add_button(label="Clear", callback=lambda: self.clear_panel(panel), width=int(40 * self.scale)) dpg.add_image_button(texture_tag="split_h_texture", callback=lambda: self.split_panel(path, 0), width=text_size, height=text_size) @@ -67,7 +265,7 @@ class PlotLayoutManager: for i, child_layout in enumerate(layout["children"]): child_path = path + [i] container_tag = self._path_to_tag(child_path, "container") - pane_width, pane_height = [(pane_sizes[i], -1), (-1, pane_sizes[i])][orientation] # fill 2nd dim up to the border + pane_width, pane_height = [(pane_sizes[i], -1), (-1, pane_sizes[i])][orientation] # fill 2nd dim up to the border with dpg.child_window(tag=container_tag, width=pane_width, height=pane_height, border=False, no_scrollbar=True): child_width, child_height = [(pane_sizes[i], height), (width, pane_sizes[i])][orientation] self._create_ui_recursive(child_layout, container_tag, child_path, child_width, child_height) @@ -137,7 +335,7 @@ class PlotLayoutManager: if path: container_tag = self._path_to_tag(path, "container") else: # Root update - container_tag = self.container_tag + container_tag = self.parent_tag self._cleanup_ui_recursive(layout, path) dpg.delete_item(container_tag, children_only=True) @@ -155,11 +353,16 @@ class PlotLayoutManager: handler_tag = f"{self._path_to_tag(path, f'grip_{i}')}_handler" if dpg.does_item_exist(handler_tag): dpg.delete_item(handler_tag) + self._created_handler_tags.discard(handler_tag) for i, child in enumerate(layout["children"]): self._cleanup_ui_recursive(child, path + [i]) def update_all_panels(self): + if self._queue_resize: + if (size := dpg.get_item_rect_size(self.parent_tag)) != [0, 0]: + self._queue_resize = False + self._resize_splits_recursive(self.layout, [], *size) for panel in self.active_panels: panel.update() @@ -181,17 +384,17 @@ class PlotLayoutManager: dpg.configure_item(container_tag, **{size_properties[orientation]: pane_sizes[i]}) child_width, child_height = [(pane_sizes[i], available_sizes[1]), (available_sizes[0], pane_sizes[i])][orientation] self._resize_splits_recursive(child_layout, child_path, child_width, child_height) - else: # leaf node/panel - adjust bar height to allow for scrollbar + else: # leaf node/panel - adjust bar height to allow for scrollbar panel_tag = self._path_to_tag(path, "panel") - if width is not None and width < int(279 * self.scale + 80): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item - dpg.configure_item(panel_tag, height=(int(13*self.scale) + 24)) + if width is not None and width < int(329 * self.scale + 64): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item + dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 24)) else: - dpg.configure_item(panel_tag, height=(int(13*self.scale) + 8)) + dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 8)) def _get_split_geometry(self, layout: dict, available_size: tuple[int, int]) -> tuple[int, int, list[int]]: orientation = layout["orientation"] num_grips = len(layout["children"]) - 1 - usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * (self.grip_size + 8 * (2-orientation)))) # approximate, scaling is weird + usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * (self.grip_size + 8 * (2 - orientation)))) # approximate, scaling is weird pane_sizes = [max(self.min_pane_size, int(usable_size * prop)) for prop in layout["proportions"]] return orientation, usable_size, pane_sizes @@ -217,16 +420,18 @@ class PlotLayoutManager: def _create_grip(self, parent_tag: str, path: list[int], grip_index: int, orientation: int): grip_tag = self._path_to_tag(path, f"grip_{grip_index}") + handler_tag = f"{grip_tag}_handler" width, height = [(self.grip_size, -1), (-1, self.grip_size)][orientation] with dpg.child_window(tag=grip_tag, parent=parent_tag, width=width, height=height, no_scrollbar=True, border=False): button_tag = dpg.add_button(label="", width=-1, height=-1) - with dpg.item_handler_registry(tag=f"{grip_tag}_handler"): + with dpg.item_handler_registry(tag=handler_tag): user_data = (path, grip_index, orientation) dpg.add_item_active_handler(callback=self._on_grip_drag, user_data=user_data) dpg.add_item_deactivated_handler(callback=self._on_grip_end, user_data=user_data) - dpg.bind_item_handler_registry(button_tag, f"{grip_tag}_handler") + dpg.bind_item_handler_registry(button_tag, handler_tag) + self._created_handler_tags.add(handler_tag) def _on_grip_drag(self, sender, app_data, user_data): path, grip_index, orientation = user_data diff --git a/tools/jotpluggler/layouts/torque-controller.yaml b/tools/jotpluggler/layouts/torque-controller.yaml new file mode 100644 index 0000000000..5503be9e64 --- /dev/null +++ b/tools/jotpluggler/layouts/torque-controller.yaml @@ -0,0 +1,128 @@ +tabs: + '0': + name: Lateral Plan Conformance + panel_layout: + type: split + orientation: 1 + proportions: + - 0.3333333333333333 + - 0.3333333333333333 + - 0.3333333333333333 + children: + - type: panel + panel: + type: timeseries + title: desired vs actual + series_paths: + - controlsState/lateralControlState/torqueState/desiredLateralAccel + - controlsState/lateralControlState/torqueState/actualLateralAccel + - type: panel + panel: + type: timeseries + title: ff vs output + series_paths: + - controlsState/lateralControlState/torqueState/f + - carState/steeringPressed + - carControl/actuators/torque + - type: panel + panel: + type: timeseries + title: vehicle speed + series_paths: + - carState/vEgo + '1': + name: Actuator Performance + panel_layout: + type: split + orientation: 1 + proportions: + - 0.3333333333333333 + - 0.3333333333333333 + - 0.3333333333333333 + children: + - type: panel + panel: + type: timeseries + title: calc vs learned latAccelFactor + series_paths: + - liveTorqueParameters/latAccelFactorFiltered + - liveTorqueParameters/latAccelFactorRaw + - carParams/lateralTuning/torque/latAccelFactor + - type: panel + panel: + type: timeseries + title: learned latAccelOffset + series_paths: + - liveTorqueParameters/latAccelOffsetRaw + - liveTorqueParameters/latAccelOffsetFiltered + - type: panel + panel: + type: timeseries + title: calc vs learned friction + series_paths: + - liveTorqueParameters/frictionCoefficientFiltered + - liveTorqueParameters/frictionCoefficientRaw + - carParams/lateralTuning/torque/friction + '2': + name: Vehicle Dynamics + panel_layout: + type: split + orientation: 1 + proportions: + - 0.3333333333333333 + - 0.3333333333333333 + - 0.3333333333333333 + children: + - type: panel + panel: + type: timeseries + title: initial vs learned steerRatio + series_paths: + - carParams/steerRatio + - liveParameters/steerRatio + - type: panel + panel: + type: timeseries + title: initial vs learned tireStiffnessFactor + series_paths: + - carParams/tireStiffnessFactor + - liveParameters/stiffnessFactor + - type: panel + panel: + type: timeseries + title: live steering angle offsets + series_paths: + - liveParameters/angleOffsetDeg + - liveParameters/angleOffsetAverageDeg + '3': + name: Controller PIF Terms + panel_layout: + type: split + orientation: 1 + proportions: + - 0.3333333333333333 + - 0.3333333333333333 + - 0.3333333333333333 + children: + - type: panel + panel: + type: timeseries + title: ff vs output + series_paths: + - carControl/actuators/torque + - controlsState/lateralControlState/torqueState/f + - carState/steeringPressed + - type: panel + panel: + type: timeseries + title: PIF terms + series_paths: + - controlsState/lateralControlState/torqueState/f + - controlsState/lateralControlState/torqueState/p + - controlsState/lateralControlState/torqueState/i + - type: panel + panel: + type: timeseries + title: road roll angle + series_paths: + - liveParameters/roll diff --git a/tools/jotpluggler/pluggle.py b/tools/jotpluggler/pluggle.py index 582a44454e..879b677514 100755 --- a/tools/jotpluggler/pluggle.py +++ b/tools/jotpluggler/pluggle.py @@ -7,10 +7,12 @@ import dearpygui.dearpygui as dpg import multiprocessing import uuid import signal +import yaml # type: ignore +from openpilot.common.swaglog import cloudlog from openpilot.common.basedir import BASEDIR from openpilot.tools.jotpluggler.data import DataManager from openpilot.tools.jotpluggler.datatree import DataTree -from openpilot.tools.jotpluggler.layout import PlotLayoutManager +from openpilot.tools.jotpluggler.layout import LayoutManager DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" @@ -64,6 +66,11 @@ class PlaybackManager: self.is_playing = False self.current_time_s = 0.0 self.duration_s = 0.0 + self.num_segments = 0 + + self.x_axis_bounds = (0.0, 0.0) # (min_time, max_time) + self.x_axis_observers = [] # callbacks for x-axis changes + self._updating_x_axis = False def set_route_duration(self, duration: float): self.duration_s = duration @@ -87,6 +94,33 @@ class PlaybackManager: dpg.configure_item("play_pause_button", texture_tag="play_texture") return self.current_time_s + def set_x_axis_bounds(self, min_time: float, max_time: float, source_panel=None): + if self._updating_x_axis: + return + + new_bounds = (min_time, max_time) + if new_bounds == self.x_axis_bounds: + return + + self.x_axis_bounds = new_bounds + self._updating_x_axis = True # prevent recursive updates + + try: + for callback in self.x_axis_observers: + try: + callback(min_time, max_time, source_panel) + except Exception as e: + print(f"Error in x-axis sync callback: {e}") + finally: + self._updating_x_axis = False + + def add_x_axis_observer(self, callback): + if callback not in self.x_axis_observers: + self.x_axis_observers.append(callback) + + def remove_x_axis_observer(self, callback): + if callback in self.x_axis_observers: + self.x_axis_observers.remove(callback) class MainController: def __init__(self, scale: float = 1.0): @@ -96,29 +130,45 @@ class MainController: self.worker_manager = WorkerManager() self._create_global_themes() self.data_tree = DataTree(self.data_manager, self.playback_manager) - self.plot_layout_manager = PlotLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, scale=self.scale) + self.layout_manager = LayoutManager(self.data_manager, self.playback_manager, self.worker_manager, scale=self.scale) self.data_manager.add_observer(self.on_data_loaded) + self._total_segments = 0 def _create_global_themes(self): - with dpg.theme(tag="global_line_theme"): + with dpg.theme(tag="line_theme"): with dpg.theme_component(dpg.mvLineSeries): scaled_thickness = max(1.0, self.scale) dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots) - with dpg.theme(tag="global_timeline_theme"): + with dpg.theme(tag="timeline_theme"): with dpg.theme_component(dpg.mvInfLineSeries): scaled_thickness = max(1.0, self.scale) dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots) dpg.add_theme_color(dpg.mvPlotCol_Line, (255, 0, 0, 128), category=dpg.mvThemeCat_Plots) + for tag, color in (("active_tab_theme", (37, 37, 38, 255)), ("inactive_tab_theme", (70, 70, 75, 255))): + with dpg.theme(tag=tag): + for cmp, target in ((dpg.mvChildWindow, dpg.mvThemeCol_ChildBg), (dpg.mvInputText, dpg.mvThemeCol_FrameBg), (dpg.mvImageButton, dpg.mvThemeCol_Button)): + with dpg.theme_component(cmp): + dpg.add_theme_color(target, color) + + with dpg.theme(tag="tab_bar_theme"): + with dpg.theme_component(dpg.mvChildWindow): + dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (51, 51, 55, 255)) + def on_data_loaded(self, data: dict): duration = data.get('duration', 0.0) self.playback_manager.set_route_duration(duration) - if data.get('reset'): + if data.get('metadata_loaded'): + self.playback_manager.num_segments = data.get('total_segments', 0) + self._total_segments = data.get('total_segments', 0) + dpg.set_value("load_status", f"Loading... 0/{self._total_segments} segments processed") + elif data.get('reset'): self.playback_manager.current_time_s = 0.0 self.playback_manager.duration_s = 0.0 self.playback_manager.is_playing = False + self._total_segments = 0 dpg.set_value("load_status", "Loading...") dpg.set_value("timeline_slider", 0.0) dpg.configure_item("timeline_slider", max_value=0.0) @@ -130,35 +180,93 @@ class MainController: dpg.configure_item("load_button", enabled=True) elif data.get('segment_added'): segment_count = data.get('segment_count', 0) - dpg.set_value("load_status", f"Loading... {segment_count} segments processed") + dpg.set_value("load_status", f"Loading... {segment_count}/{self._total_segments} segments processed") dpg.configure_item("timeline_slider", max_value=duration) + def save_layout_to_yaml(self, filepath: str): + layout_dict = self.layout_manager.to_dict() + with open(filepath, 'w') as f: + yaml.dump(layout_dict, f, default_flow_style=False, sort_keys=False) + + def load_layout_from_yaml(self, filepath: str): + with open(filepath) as f: + layout_dict = yaml.safe_load(f) + self.layout_manager.clear_and_load_from_dict(layout_dict) + self.layout_manager.create_ui("main_plot_area") + + def save_layout_dialog(self): + if dpg.does_item_exist("save_layout_dialog"): + dpg.delete_item("save_layout_dialog") + with dpg.file_dialog( + callback=self._save_layout_callback, tag="save_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale), + default_filename="layout", default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts") + ): + dpg.add_file_extension(".yaml") + + def load_layout_dialog(self): + if dpg.does_item_exist("load_layout_dialog"): + dpg.delete_item("load_layout_dialog") + with dpg.file_dialog( + callback=self._load_layout_callback, tag="load_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale), + default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts") + ): + dpg.add_file_extension(".yaml") + + def _save_layout_callback(self, sender, app_data): + filepath = app_data['file_path_name'] + try: + self.save_layout_to_yaml(filepath) + dpg.set_value("load_status", f"Layout saved to {os.path.basename(filepath)}") + except Exception: + dpg.set_value("load_status", "Error saving layout") + cloudlog.exception(f"Error saving layout to {filepath}") + dpg.delete_item("save_layout_dialog") + + def _load_layout_callback(self, sender, app_data): + filepath = app_data['file_path_name'] + try: + self.load_layout_from_yaml(filepath) + dpg.set_value("load_status", f"Layout loaded from {os.path.basename(filepath)}") + except Exception: + dpg.set_value("load_status", "Error loading layout") + cloudlog.exception(f"Error loading layout from {filepath}:") + dpg.delete_item("load_layout_dialog") + def setup_ui(self): with dpg.texture_registry(): script_dir = os.path.dirname(os.path.realpath(__file__)) - for image in ["play", "pause", "x", "split_h", "split_v"]: + for image in ["play", "pause", "x", "split_h", "split_v", "plus"]: texture = dpg.load_image(os.path.join(script_dir, "assets", f"{image}.png")) dpg.add_static_texture(width=texture[0], height=texture[1], default_value=texture[3], tag=f"{image}_texture") with dpg.window(tag="Primary Window"): with dpg.group(horizontal=True): # Left panel - Data tree - with dpg.child_window(label="Sidebar", width=300 * self.scale, tag="sidebar_window", border=True, resizable_x=True): + with dpg.child_window(label="Sidebar", width=int(300 * self.scale), tag="sidebar_window", border=True, resizable_x=True): with dpg.group(horizontal=True): - dpg.add_input_text(tag="route_input", width=-75 * self.scale, hint="Enter route name...") + dpg.add_input_text(tag="route_input", width=int(-75 * self.scale), hint="Enter route name...") dpg.add_button(label="Load", callback=self.load_route, tag="load_button", width=-1) dpg.add_text("Ready to load route", tag="load_status") dpg.add_separator() + + with dpg.table(header_row=False, policy=dpg.mvTable_SizingStretchProp): + dpg.add_table_column(init_width_or_weight=0.5) + dpg.add_table_column(init_width_or_weight=0.5) + with dpg.table_row(): + dpg.add_button(label="Save Layout", callback=self.save_layout_dialog, width=-1) + dpg.add_button(label="Load Layout", callback=self.load_layout_dialog, width=-1) + dpg.add_separator() + self.data_tree.create_ui("sidebar_window") # Right panel - Plots and timeline with dpg.group(tag="right_panel"): - with dpg.child_window(label="Plot Window", border=True, height=-(32 + 13 * self.scale), tag="main_plot_area"): - self.plot_layout_manager.create_ui("main_plot_area") + with dpg.child_window(label="Plot Window", border=True, height=int(-(32 + 13 * self.scale)), tag="main_plot_area"): + self.layout_manager.create_ui("main_plot_area") with dpg.child_window(label="Timeline", border=True): - with dpg.table(header_row=False, borders_innerH=False, borders_innerV=False, borders_outerH=False, borders_outerV=False): + with dpg.table(header_row=False): btn_size = int(13 * self.scale) dpg.add_table_column(width_fixed=True, init_width_or_weight=(btn_size + 8)) # Play button dpg.add_table_column(width_stretch=True) # Timeline slider @@ -174,7 +282,7 @@ class MainController: dpg.set_primary_window("Primary Window", True) def on_plot_resize(self, sender, app_data, user_data): - self.plot_layout_manager.on_viewport_resize() + self.layout_manager.on_viewport_resize() def load_route(self): route_name = dpg.get_value("route_input").strip() @@ -196,7 +304,7 @@ class MainController: if not dpg.is_item_active("timeline_slider"): dpg.set_value("timeline_slider", new_time) - self.plot_layout_manager.update_all_panels() + self.layout_manager.update_all_panels() dpg.set_value("fps_counter", f"{dpg.get_frame_rate():.1f} FPS") @@ -204,7 +312,7 @@ class MainController: self.worker_manager.shutdown() -def main(route_to_load=None): +def main(route_to_load=None, layout_to_load=None): dpg.create_context() # TODO: find better way of calculating display scaling @@ -215,8 +323,9 @@ def main(route_to_load=None): scale = 1 with dpg.font_registry(): - default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale)) + default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale * 2)) # 2x then scale for hidpi dpg.bind_font(default_font) + dpg.set_global_font_scale(0.5) viewport_width, viewport_height = int(1200 * scale), int(800 * scale) mouse_x, mouse_y = pyautogui.position() # TODO: find better way of creating the window where the user is (default dpg behavior annoying on multiple displays) @@ -228,6 +337,14 @@ def main(route_to_load=None): controller = MainController(scale=scale) controller.setup_ui() + if layout_to_load: + try: + controller.load_layout_from_yaml(layout_to_load) + print(f"Loaded layout from {layout_to_load}") + except Exception as e: + print(f"Failed to load layout from {layout_to_load}: {e}") + cloudlog.exception(f"Error loading layout from {layout_to_load}") + if route_to_load: dpg.set_value("route_input", route_to_load) controller.load_route() @@ -246,7 +363,8 @@ def main(route_to_load=None): if __name__ == "__main__": parser = argparse.ArgumentParser(description="A tool for visualizing openpilot logs.") parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") + parser.add_argument("--layout", type=str, help="Path to YAML layout file to load on startup") parser.add_argument("route", nargs='?', default=None, help="Optional route name to load on startup.") args = parser.parse_args() route = DEMO_ROUTE if args.demo else args.route - main(route_to_load=route) + main(route_to_load=route, layout_to_load=args.layout) diff --git a/tools/jotpluggler/views.py b/tools/jotpluggler/views.py index 4af9a102ac..1c4d9a8f3c 100644 --- a/tools/jotpluggler/views.py +++ b/tools/jotpluggler/views.py @@ -33,6 +33,15 @@ class ViewPanel(ABC): def update(self): pass + @abstractmethod + def to_dict(self) -> dict: + pass + + @classmethod + @abstractmethod + def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager): + pass + class TimeSeriesPanel(ViewPanel): def __init__(self, data_manager, playback_manager, worker_manager, panel_id: str | None = None): @@ -51,18 +60,37 @@ class TimeSeriesPanel(ViewPanel): self._update_lock = threading.RLock() self._results_deque: deque[tuple[str, list, list]] = deque() self._new_data = False + self._last_x_limits = (0.0, 0.0) + self._queued_x_sync: tuple | None = None + self._queued_reallow_x_zoom = False + self._total_segments = self.playback_manager.num_segments + + def to_dict(self) -> dict: + return { + "type": "timeseries", + "title": self.title, + "series_paths": list(self._series_data.keys()) + } + + @classmethod + def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager): + panel = cls(data_manager, playback_manager, worker_manager) + panel.title = data.get("title", "Time Series Plot") + panel._series_data = {path: (np.array([]), np.array([])) for path in data.get("series_paths", [])} + return panel def create_ui(self, parent_tag: str): self.data_manager.add_observer(self.on_data_loaded) + self.playback_manager.add_x_axis_observer(self._on_x_axis_sync) with dpg.plot(height=-1, width=-1, tag=self.plot_tag, parent=parent_tag, drop_callback=self._on_series_drop, payload_type="TIMESERIES_PAYLOAD"): dpg.add_plot_legend() dpg.add_plot_axis(dpg.mvXAxis, no_label=True, tag=self.x_axis_tag) dpg.add_plot_axis(dpg.mvYAxis, no_label=True, tag=self.y_axis_tag) timeline_series_tag = dpg.add_inf_line_series(x=[0], label="Timeline", parent=self.y_axis_tag, tag=self.timeline_indicator_tag) - dpg.bind_item_theme(timeline_series_tag, "global_timeline_theme") + dpg.bind_item_theme(timeline_series_tag, "timeline_theme") - for series_path in list(self._series_data.keys()): - self.add_series(series_path) + self._new_data = True + self._queued_x_sync = self.playback_manager.x_axis_bounds self._ui_created = True def update(self): @@ -70,11 +98,42 @@ class TimeSeriesPanel(ViewPanel): if not self._ui_created: return + if self._queued_x_sync: + min_time, max_time = self._queued_x_sync + self._queued_x_sync = None + dpg.set_axis_limits(self.x_axis_tag, min_time, max_time) + self._last_x_limits = (min_time, max_time) + self._fit_y_axis(min_time, max_time) + self._queued_reallow_x_zoom = True # must wait a frame before allowing user changes so that axis limits take effect + return + + if self._queued_reallow_x_zoom: + self._queued_reallow_x_zoom = False + if tuple(dpg.get_axis_limits(self.x_axis_tag)) == self._last_x_limits: + dpg.set_axis_limits_auto(self.x_axis_tag) + else: + self._queued_x_sync = self._last_x_limits # retry, likely too early + return + if self._new_data: # handle new data in main thread self._new_data = False + if self._total_segments > 0: + dpg.set_axis_limits_constraints(self.x_axis_tag, -10, self._total_segments * 60 + 10) + self._fit_y_axis(*dpg.get_axis_limits(self.x_axis_tag)) for series_path in list(self._series_data.keys()): self.add_series(series_path, update=True) + current_limits = dpg.get_axis_limits(self.x_axis_tag) + # downsample if plot zoom changed significantly + plot_duration = current_limits[1] - current_limits[0] + if plot_duration > self._last_plot_duration * 2 or plot_duration < self._last_plot_duration * 0.5: + self._downsample_all_series(plot_duration) + # sync x-axis if changed by user + if self._last_x_limits != current_limits: + self.playback_manager.set_x_axis_bounds(current_limits[0], current_limits[1], source_panel=self) + self._last_x_limits = current_limits + self._fit_y_axis(current_limits[0], current_limits[1]) + while self._results_deque: # handle downsampled results in main thread results = self._results_deque.popleft() for series_path, downsampled_time, downsampled_values in results: @@ -96,10 +155,45 @@ class TimeSeriesPanel(ViewPanel): if dpg.does_item_exist(series_tag): dpg.configure_item(series_tag, label=f"{series_path}: {formatted_value}") - # downsample if plot zoom changed significantly - plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0] - if plot_duration > self._last_plot_duration * 2 or plot_duration < self._last_plot_duration * 0.5: - self._downsample_all_series(plot_duration) + def _on_x_axis_sync(self, min_time: float, max_time: float, source_panel): + with self._update_lock: + if source_panel != self: + self._queued_x_sync = (min_time, max_time) + + def _fit_y_axis(self, x_min: float, x_max: float): + if not self._series_data: + dpg.set_axis_limits(self.y_axis_tag, -1, 1) + return + + global_min = float('inf') + global_max = float('-inf') + found_data = False + + for time_array, value_array in self._series_data.values(): + if len(time_array) == 0: + continue + start_idx, end_idx = np.searchsorted(time_array, [x_min, x_max]) + end_idx = min(end_idx, len(time_array) - 1) + if start_idx <= end_idx: + y_slice = value_array[start_idx:end_idx + 1] + series_min, series_max = np.min(y_slice), np.max(y_slice) + global_min = min(global_min, series_min) + global_max = max(global_max, series_max) + found_data = True + + if not found_data: + dpg.set_axis_limits(self.y_axis_tag, -1, 1) + return + + if global_min == global_max: + padding = max(abs(global_min) * 0.1, 1.0) + y_min, y_max = global_min - padding, global_max + padding + else: + range_size = global_max - global_min + padding = range_size * 0.1 + y_min, y_max = global_min - padding, global_max + padding + + dpg.set_axis_limits(self.y_axis_tag, y_min, y_max) def _downsample_all_series(self, plot_duration): plot_width = dpg.get_item_rect_size(self.plot_tag)[0] @@ -136,15 +230,15 @@ class TimeSeriesPanel(ViewPanel): dpg.set_value(series_tag, (time_array, value_array.astype(float))) else: line_series_tag = dpg.add_line_series(x=time_array, y=value_array.astype(float), label=series_path, parent=self.y_axis_tag, tag=series_tag) - dpg.bind_item_theme(line_series_tag, "global_line_theme") - dpg.fit_axis_data(self.x_axis_tag) - dpg.fit_axis_data(self.y_axis_tag) + dpg.bind_item_theme(line_series_tag, "line_theme") + self._fit_y_axis(*dpg.get_axis_limits(self.x_axis_tag)) plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0] self._downsample_all_series(plot_duration) def destroy_ui(self): with self._update_lock: self.data_manager.remove_observer(self.on_data_loaded) + self.playback_manager.remove_x_axis_observer(self._on_x_axis_sync) if dpg.does_item_exist(self.plot_tag): dpg.delete_item(self.plot_tag) self._ui_created = False @@ -165,7 +259,12 @@ class TimeSeriesPanel(ViewPanel): del self._series_data[series_path] def on_data_loaded(self, data: dict): - self._new_data = True + with self._update_lock: + self._new_data = True + if data.get('metadata_loaded'): + self._total_segments = data.get('total_segments', 0) + limits = (-10, self._total_segments * 60 + 10) + self._queued_x_sync = limits def _on_series_drop(self, sender, app_data, user_data): self.add_series(app_data) diff --git a/tools/lib/filereader.py b/tools/lib/filereader.py index 02f5fd1b95..ee9ee294bb 100644 --- a/tools/lib/filereader.py +++ b/tools/lib/filereader.py @@ -2,7 +2,7 @@ import os import posixpath import socket from functools import cache -from openpilot.common.retry import retry +from openpilot.common.utils import retry from urllib.parse import urlparse from openpilot.tools.lib.url_file import URLFile diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py index 8d0870171f..ee75a8b1ce 100644 --- a/tools/lib/tests/test_logreader.py +++ b/tools/lib/tests/test_logreader.py @@ -72,6 +72,7 @@ class TestLogReader: (f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '|')}", ALL_SEGS), (f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '%7C')}", ALL_SEGS), ]) + @pytest.mark.skip("this got flaky. internet tests are stupid.") def test_indirect_parsing(self, identifier, expected): parsed = parse_indirect(identifier) sr = SegmentRange(parsed) diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index e80ba1399d..31c1e0ff11 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -7,7 +7,7 @@ from urllib3 import PoolManager, Retry from urllib3.response import BaseHTTPResponse from urllib3.util import Timeout -from openpilot.common.file_helpers import atomic_write_in_dir +from openpilot.common.utils import atomic_write_in_dir from openpilot.system.hardware.hw import Paths # Cache chunk size diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index 19ef77e01c..0ae0b35359 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -34,13 +34,11 @@ fi brew bundle --file=- <<-EOS brew "git-lfs" -brew "zlib" brew "capnp" brew "coreutils" brew "eigen" brew "ffmpeg" brew "glfw" -brew "libarchive" brew "libusb" brew "libtool" brew "llvm" @@ -50,7 +48,6 @@ brew "zeromq" cask "gcc-arm-embedded" brew "portaudio" brew "gcc@13" -cask "font-noto-color-emoji" EOS echo "[ ] finished brew install t=$SECONDS" diff --git a/tools/op.sh b/tools/op.sh index 54ff8e97e9..8b5062ad9b 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -284,7 +284,7 @@ function op_venv() { function op_adb() { op_before_cmd - op_run_command tools/scripts/adb_ssh.sh + op_run_command tools/scripts/adb_ssh.sh "$@" } function op_ssh() { @@ -366,9 +366,11 @@ function op_switch() { BRANCH="$1" git config --replace-all remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + git submodule deinit --all --force git fetch "$REMOTE" "$BRANCH" git checkout -f FETCH_HEAD git checkout -B "$BRANCH" --track "$REMOTE"/"$BRANCH" + git submodule deinit --all --force git reset --hard "${REMOTE}/${BRANCH}" git clean -df git submodule update --init --recursive diff --git a/tools/replay/README.md b/tools/replay/README.md index 72216e2cc8..794c08f6a3 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -88,7 +88,7 @@ To visualize the replay within the openpilot UI, run the following commands: ```bash tools/replay/replay -cd selfdrive/ui && ./ui +cd selfdrive/ui && ./ui.py ``` ## Work with plotjuggler @@ -110,7 +110,7 @@ simply replay a route using the `--dcam` and `--ecam` flags: cd tools/replay && ./replay --demo --dcam --ecam # then start watch3 -cd selfdrive/ui && ./watch3 +cd selfdrive/ui && ./watch3.py ``` ![](https://i.imgur.com/IeaOdAb.png) diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index e9cd090446..a5f0f748c7 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -40,7 +40,7 @@ struct DecoderManager { std::unique_ptr decoder; #ifndef __APPLE__ - if (Hardware::TICI() && hw_decoder) { + if (!Hardware::PC() && hw_decoder) { decoder = std::make_unique(); } else #endif diff --git a/tools/replay/main.cc b/tools/replay/main.cc index f950985075..4609012194 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -30,7 +30,7 @@ Options: --qcam Load qcamera --no-hw-decoder Disable HW video decoding --no-vipc Do not output video - --all Output all messages including uiDebug, userBookmark + --all Output all messages including bookmarkButton, uiDebug, userBookmark -h, --help Show this help message )"; diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 7ecad82873..c9ab7e7e2b 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -20,7 +20,7 @@ Replay::Replay(const std::string &route, std::vector allow, std::ve std::signal(SIGUSR1, interrupt_sleep_handler); if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) { - block.insert(block.end(), {"uiDebug", "userBookmark"}); + block.insert(block.end(), {"bookmarkButton", "uiDebug", "userBookmark"}); } setupServices(allow, block); setupSegmentManager(!allow.empty() || !block.empty()); diff --git a/tools/scripts/adb_ssh.sh b/tools/scripts/adb_ssh.sh index 43c8e07de6..ad65693722 100755 --- a/tools/scripts/adb_ssh.sh +++ b/tools/scripts/adb_ssh.sh @@ -1,7 +1,42 @@ #!/usr/bin/env bash -set -e +set -euo pipefail -# this is a little nicer than "adb shell" since -# "adb shell" doesn't do full terminal emulation +# Forward all openpilot service ports +mapfile -t SERVICE_PORTS < <(python3 - <<'PY' +from cereal.services import SERVICE_LIST + +FNV_PRIME = 0x100000001b3 +FNV_OFFSET_BASIS = 0xcbf29ce484222325 +START_PORT = 8023 +MAX_PORT = 65535 +PORT_RANGE = MAX_PORT - START_PORT +MASK = 0xffffffffffffffff + +def fnv1a(endpoint: str) -> int: + h = FNV_OFFSET_BASIS + for b in endpoint.encode(): + h ^= b + h = (h * FNV_PRIME) & MASK + return h + +ports = set() +for name in SERVICE_LIST.keys(): + port = START_PORT + fnv1a(name) % PORT_RANGE + ports.add((name, port)) + +for name, port in sorted(ports): + print(f"{name} {port}") +PY +) + +for entry in "${SERVICE_PORTS[@]}"; do + name="${entry% *}" + port="${entry##* }" + adb forward "tcp:${port}" "tcp:${port}" > /dev/null +done + +# Forward SSH port first for interactive shell access. adb forward tcp:2222 tcp:22 -ssh comma@localhost -p 2222 + +# SSH! +ssh comma@localhost -p 2222 "$@" diff --git a/tools/scripts/ssh.py b/tools/scripts/ssh.py index 0429799a2e..e454afb691 100755 --- a/tools/scripts/ssh.py +++ b/tools/scripts/ssh.py @@ -50,7 +50,7 @@ if __name__ == "__main__": if args.debug: command += ["-v"] command += [ - f"comma@{dongle_id}", + f"comma@comma-{dongle_id}", ] if args.debug: print(" ".join([f"'{c}'" if " " in c else c for c in command])) diff --git a/uv.lock b/uv.lock index b871cc8da4..c34a6d9b71 100644 --- a/uv.lock +++ b/uv.lock @@ -152,21 +152,21 @@ wheels = [ [[package]] name = "azure-core" -version = "1.35.0" +version = "1.35.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/6b/2653adc0f33adba8f11b1903701e6b1c10d34ce5d8e25dfa13a422f832b0/azure_core-1.35.1.tar.gz", hash = "sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562", size = 345290, upload-time = "2025-09-11T22:58:04.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" }, + { url = "https://files.pythonhosted.org/packages/27/52/805980aa1ba18282077c484dba634ef0ede1e84eec8be9c92b2e162d0ed6/azure_core-1.35.1-py3-none-any.whl", hash = "sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b", size = 211800, upload-time = "2025-09-11T22:58:06.281Z" }, ] [[package]] name = "azure-identity" -version = "1.24.0" +version = "1.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -175,9 +175,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/44/f3ee20bacb220b6b4a2b0a6cf7e742eecb383a5ccf604dd79ec27c286b7e/azure_identity-1.24.0.tar.gz", hash = "sha256:6c3a40b2a70af831e920b89e6421e8dcd4af78a0cb38b9642d86c67643d4930c", size = 271630, upload-time = "2025-08-07T22:27:36.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/9e/4c9682a286c3c89e437579bd9f64f311020e5125c1321fd3a653166b5716/azure_identity-1.25.0.tar.gz", hash = "sha256:4177df34d684cddc026e6cf684e1abb57767aa9d84e7f2129b080ec45eee7733", size = 278507, upload-time = "2025-09-12T01:30:04.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/74/17428cb429e8d52f6d0d69ed685f4760a545cb0156594963a9337b53b6c9/azure_identity-1.24.0-py3-none-any.whl", hash = "sha256:9e04997cde0ab02ed66422c74748548e620b7b29361c72ce622acab0267ff7c4", size = 187890, upload-time = "2025-08-07T22:27:38.033Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/81683b6756676a22e037b209695b08008258e603f7e47c56834029c5922a/azure_identity-1.25.0-py3-none-any.whl", hash = "sha256:becaec086bbdf8d1a6aa4fb080c2772a0f824a97d50c29637ec8cc4933f1e82d", size = 190861, upload-time = "2025-09-12T01:30:06.474Z" }, ] [[package]] @@ -197,25 +197,25 @@ wheels = [ [[package]] name = "casadi" -version = "3.7.1" +version = "3.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/21/1b4ea75dbddfcdbc7cd70d83147dde72899667097c0c05b288fd7015ef10/casadi-3.7.1.tar.gz", hash = "sha256:12577155cd3cd79ba162381bfed6add1541bc770ba3f1f1334e4eb159d9b39ba", size = 6065586, upload-time = "2025-07-23T21:47:56.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/62/1e98662024915ecb09c6894c26a3f497f4afa66570af3f53db4651fc45f1/casadi-3.7.2.tar.gz", hash = "sha256:b4d7bd8acdc4180306903ae1c9eddaf41be2a3ae2fa7154c57174ae64acdc60d", size = 6053600, upload-time = "2025-09-10T10:05:49.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/4d/cfe092b2deb65dbf913d38148e30dfe41bbe9f289f892ea7a5d3526532d2/casadi-3.7.1-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:c13fafdff0cdf73392fe2002245befe7496b5ac99b70adb64babff0099068c8a", size = 47100775, upload-time = "2025-07-23T19:56:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/13/41/c4ad627e447a083fbfd2826f1f4684223f0072fdc49f58157ca6a6ce0a77/casadi-3.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:74d9c6bad3c7cb347494d52cc3e6faf3fbf3300046fab0e1ddc9439d3fd68486", size = 42282155, upload-time = "2025-07-23T19:59:11.191Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e2/53733933d454657e319cc8074e004ec46660c4797cbffcaf7695a874b0da/casadi-3.7.1-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:6fbc0803e6c7ffb9ba9a41ba38e7c1dadee73835edbacacce3530343f078f3f9", size = 47272638, upload-time = "2025-07-23T20:03:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/e1/72/8002d4fe64325842eb6a7f7ac1e4574c02584e85e5b9d93a08a789095505/casadi-3.7.1-cp311-none-manylinux2014_i686.whl", hash = "sha256:38a16b0c7caff5297df91c7e3a65091824979367e6fc8e6eb0e5ab9f1f832af8", size = 72418203, upload-time = "2025-07-23T20:07:18.681Z" }, - { url = "https://files.pythonhosted.org/packages/96/e5/065287b78c4f6a00a010d7c3f5316df1b95f7c5bda644f2151b54a79a964/casadi-3.7.1-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:2dd75cc1b5ca8009586186e4a63e0cdd83a672bf308edc75ef84f9c896cd971d", size = 75558461, upload-time = "2025-07-23T21:33:20.771Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4c/084d2f3bca33d0b3d509be17c3b678f6bcc03f3409d42e88750cb1e44966/casadi-3.7.1-cp311-none-win_amd64.whl", hash = "sha256:3ab68c6dd621b5f150635bf06aa8e1697cae2fbe0137e9d47270c54d305c334c", size = 50987255, upload-time = "2025-07-23T21:34:20.349Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a3/65942d3e0f7589a72e69dd9dee575963ecf51504e985f132bce8b0d24988/casadi-3.7.1-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:4a29ae3212b0ae6931d439ca3bfa2ac279826e99c2c9a443704e84021933ab76", size = 47102059, upload-time = "2025-07-23T20:36:47.174Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f4/dc371199583f6b154ba0966c4132418737a2d2e019c9f6ad227ebd279684/casadi-3.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:70edf1df3fc83401eb5a2f7053ae8394f4e612e61c2ad6de088e3d76d0ec845e", size = 42282702, upload-time = "2025-07-23T20:39:41.499Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ff/5f8a9378cd7b322d1dcc8b8aa9fbc454ad57152b754c94b50ffdc67254a0/casadi-3.7.1-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:a1717bbd07a6eb98acddf5ea5da7e78a9b85b75dbabb55190cc6b0f2812ef39d", size = 47271636, upload-time = "2025-07-23T20:42:59.961Z" }, - { url = "https://files.pythonhosted.org/packages/e1/11/e1b6a76bf288fc86dc8919cbcd2d3a654e7ead4240d9928deb8ed4ee91da/casadi-3.7.1-cp312-none-manylinux2014_i686.whl", hash = "sha256:57ae29d16af0716ebc15d161bcf0bcd97d35631b10c0e80838290d249cac80d7", size = 72410197, upload-time = "2025-07-23T21:35:35.509Z" }, - { url = "https://files.pythonhosted.org/packages/c1/76/bb43ff625066b178132172d41db0c5c962b4af9db15c88b8a23e65376d18/casadi-3.7.1-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:756f3d07d3414dc54f05ebec883ac3db6e307bfe661719d188340504b6bed420", size = 75552890, upload-time = "2025-07-23T21:37:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/11/0c/8026b0b238e00a2e63c7142505cbcd0931b4db4140d55c83962f9d8e0b02/casadi-3.7.1-cp312-none-win_amd64.whl", hash = "sha256:b228fbcd1f41eb8d5405dbc1e0ecb780daf89808392706bb77fd2b240f8a437e", size = 50984453, upload-time = "2025-07-23T21:38:18.21Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/d5e3058775ec8e24a01eb74d36099493b872536ef9e39f1e49624b977778/casadi-3.7.2-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:f43b0562d05a5e6e81f1885fc4ae426c382e36eebfd8d27f1baff6052178a9b0", size = 47115880, upload-time = "2025-09-10T07:52:24.399Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cf/4af27e010d599a5419129d34fdde41637029a1cca2a40bef0965d6d52228/casadi-3.7.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:70add3334b437b60a9bc0f864d094350f1a4fcbf9e8bafec870b61aed64674df", size = 42293337, upload-time = "2025-09-10T08:03:32.556Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4c/d1a50cc840103e00effcbaf8e911b6b3fb6ba2c8f4025466f524854968ed/casadi-3.7.2-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:392d3367a4b33cf223013dad8122a0e549da40b1702a5375f82f85b563e5c0cf", size = 47277175, upload-time = "2025-09-10T08:04:08.811Z" }, + { url = "https://files.pythonhosted.org/packages/be/29/6e5714d124e6ddafbccc3ed774ca603081caa1175c7f0e1c52484184dfb3/casadi-3.7.2-cp311-none-manylinux2014_i686.whl", hash = "sha256:2ce09e0ced6df33048dccd582b5cfa2c9ff5193b12858b2584078afc17761905", size = 72438460, upload-time = "2025-09-10T08:05:02.769Z" }, + { url = "https://files.pythonhosted.org/packages/23/32/ac1f3999273aa4aae48516f6f4b7b267e0cc70d8527866989798cb81312f/casadi-3.7.2-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:5086799a46d10ba884b72fd02c21be09dae52cbc189272354a5d424791b55f37", size = 75574474, upload-time = "2025-09-10T08:06:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/68/78/7fd10709504c1757f70db3893870a891fcb9f1ec9f05e8ef2e3f3b9d7e2f/casadi-3.7.2-cp311-none-win_amd64.whl", hash = "sha256:72aa5727417d781ed216f16b5e93c6ddca5db27d83b0015a729e8ad570cdc465", size = 50994144, upload-time = "2025-09-10T08:06:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/65/c8/689d085447b1966f42bdb8aa4fbebef49a09697dbee32ab02a865c17ac1b/casadi-3.7.2-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:309ea41a69c9230390d349b0dd899c6a19504d1904c0756bef463e47fb5c8f9a", size = 47116756, upload-time = "2025-09-10T07:53:00.931Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c0/3c4704394a6fd4dfb2123a4fd71ba64a001f340670a3eba45be7a19ac736/casadi-3.7.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6033381234db810b2247d16c6352e679a009ec4365d04008fc768866e011ed58", size = 42293718, upload-time = "2025-09-10T08:07:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/4cf05469ddf8544da5e92f359f96d716a97e7482999f085a632bc4ef344a/casadi-3.7.2-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:732f2804d0766454bb75596339e4f2da6662ffb669621da0f630ed4af9e83d6a", size = 47276175, upload-time = "2025-09-10T08:08:09.29Z" }, + { url = "https://files.pythonhosted.org/packages/82/08/b5f57fea03128efd5c860673b6ac44776352e6c1af862b8177f4c503fffe/casadi-3.7.2-cp312-none-manylinux2014_i686.whl", hash = "sha256:cf17298ff0c162735bdf9bf72b765c636ae732130604017a3b52e26e35402857", size = 72430454, upload-time = "2025-09-10T08:09:10.781Z" }, + { url = "https://files.pythonhosted.org/packages/24/ab/d7233c915b12c005655437c6c4cf0ae46cbbb2b20d743cb5e4881ad3104a/casadi-3.7.2-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:cde616930fa1440ad66f1850670399423edd37354eed9b12e74b3817b98d1187", size = 75568903, upload-time = "2025-09-10T08:10:07.108Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/5b984124f539656efdf079f3d8f09d73667808ec8d0546e6bce6dc60ade6/casadi-3.7.2-cp312-none-win_amd64.whl", hash = "sha256:81d677d2b020c1307c1eb25eae15686e5de199bb066828c3eaabdfaaaf457ffd", size = 50991347, upload-time = "2025-09-10T08:10:46.629Z" }, ] [[package]] @@ -229,36 +229,38 @@ wheels = [ [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, ] [[package]] @@ -294,14 +296,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, ] [[package]] @@ -415,31 +417,31 @@ wheels = [ [[package]] name = "cython" -version = "3.1.3" +version = "3.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/ab/915337fb39ab4f4539a313df38fc69938df3bf14141b90d61dfd5c2919de/cython-3.1.3.tar.gz", hash = "sha256:10ee785e42328924b78f75a74f66a813cb956b4a9bc91c44816d089d5934c089", size = 3186689, upload-time = "2025-08-13T06:19:13.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/f6/d762df1f436a0618455d37f4e4c4872a7cd0dcfc8dec3022ee99e4389c69/cython-3.1.4.tar.gz", hash = "sha256:9aefefe831331e2d66ab31799814eae4d0f8a2d246cbaaaa14d1be29ef777683", size = 3190778, upload-time = "2025-09-16T07:20:33.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/51/54f5d1bed7b7d003d0584996a030542497aeb05b9241782c217ea71061f5/cython-3.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b3b2f6587b42efdece2d174a2aa4234da4524cc6673f3955c2e62b60c6d11fd", size = 3002973, upload-time = "2025-08-13T06:19:50.777Z" }, - { url = "https://files.pythonhosted.org/packages/05/07/b4043fed60070ee21b0d9ff3a877d2ecdc79231e55119ce852b79b690306/cython-3.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:963cf640d049fcca1cefd62d1653f859892d6dc8e4d958eb49a5babc491de6a1", size = 2865389, upload-time = "2025-08-13T06:19:52.675Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e3/67d349b5310a40f281e153e826ca24d9f7ba2997c06800eda781253dccfd/cython-3.1.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2ab05d1bf2d5522ecff35d94ca233b77db2300413597c3ca0b6448377fa4bd7c", size = 3410316, upload-time = "2025-08-13T06:19:55.217Z" }, - { url = "https://files.pythonhosted.org/packages/49/c4/84aae921135174111091547fa726ea5f8bba718cd12b2589a70c2aab8d5c/cython-3.1.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd517e3be052fb49443585b01f02f46080b3408e32c1108a0fdc4cc25b3c9d30", size = 3189568, upload-time = "2025-08-13T06:19:57.503Z" }, - { url = "https://files.pythonhosted.org/packages/e2/85/1bf18883f1a1f656ad83a671e54553caedb1ee2f39a3e792a14aa82557a2/cython-3.1.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a48e2180d74e3c528561d85b48f9a939a429537f9ea8aac7fb16180e7bff47e2", size = 3312649, upload-time = "2025-08-13T06:19:59.894Z" }, - { url = "https://files.pythonhosted.org/packages/68/da/cc1373decc0d14a25f1b434f47de5e27696f3092319aa5e19fcf84157415/cython-3.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7c9daa90b15f59aa2a0d638ac1b36777a7e80122099952a0295c71190ce14bc", size = 3203821, upload-time = "2025-08-13T06:20:02.124Z" }, - { url = "https://files.pythonhosted.org/packages/0a/32/e10582d6f7b02ee63607f388dbbd34e329c54559c85961ddc5c655266519/cython-3.1.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:08ac646ff42781827f23b7a9b61669cdb92055f52724cd8cbe0e1defc56fce2e", size = 3426853, upload-time = "2025-08-13T06:20:04.474Z" }, - { url = "https://files.pythonhosted.org/packages/e0/da/5b0d1b5a4c7622ec812ad9374c9c6b426d69818f13f51e36f4f25ca01c86/cython-3.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bb0e0e7fceaffa22e4dc9600f7f75998eef5cc6ac5a8c0733b482851ba765ef2", size = 3328872, upload-time = "2025-08-13T06:20:06.834Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5f/f102f6c8d27338f0baf094754c67f920828a19612053abc903e66f84506f/cython-3.1.3-cp311-cp311-win32.whl", hash = "sha256:42b1c3ebe36a52e2a8e939c0651e9ca5d30b81d03f800bbf0499deb0503ab565", size = 2480850, upload-time = "2025-08-13T06:20:08.988Z" }, - { url = "https://files.pythonhosted.org/packages/b7/60/415d0f0f7c2e227707baec11c968387d33507d2eb7f26a2950a323c705e5/cython-3.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:34a973844998281951bf54cdd0b6a9946ba03ba94580820738583a00da167d8f", size = 2712560, upload-time = "2025-08-13T06:20:11.877Z" }, - { url = "https://files.pythonhosted.org/packages/79/26/f433fdfd5182b9231819a99acc49a1f856669532306e7a38dce63ba7297e/cython-3.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:849ef3d15d4354e5f74cdb6d3c80d80b03209b3bf1f4ff93315890b19da18944", size = 3014237, upload-time = "2025-08-13T06:20:13.77Z" }, - { url = "https://files.pythonhosted.org/packages/e5/6c/1bebf44f5f177f8c750e608f82c08cd699b8f28cc233e799379bfcf2a2c2/cython-3.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93dd0f62a3f8e93166d8584f8b243180d681ba8fed1f530b55d5f70c348c5797", size = 2844261, upload-time = "2025-08-13T06:20:15.619Z" }, - { url = "https://files.pythonhosted.org/packages/c7/74/983005ce5954f6dc8360b105a561e51a60ea619c53296afac98253d1c9d7/cython-3.1.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ff4a2cb84798faffb3988bd94636c3ad31a95ff44ef017f09121abffc56f84cf", size = 3388846, upload-time = "2025-08-13T06:20:17.679Z" }, - { url = "https://files.pythonhosted.org/packages/68/50/dbe7edefe9b652bc72d49da07785173e89197b9a2d64a2ac45da9795eccf/cython-3.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b05319e36f34d5388deea5cc2bcfd65f9ebf76f4ea050829421a69625dbba4a", size = 3167022, upload-time = "2025-08-13T06:20:19.863Z" }, - { url = "https://files.pythonhosted.org/packages/4a/55/b50548b77203e22262002feae23a172f95282b4b8deb5ad104f08e3dc20d/cython-3.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac902a17934a6da46f80755f49413bc4c03a569ae3c834f5d66da7288ba7e6c", size = 3317782, upload-time = "2025-08-13T06:20:21.962Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1b/20a97507d528dc330d67be4fbad6bc767c681d56192bce8f7117a187b2ad/cython-3.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7d7a555a864b1b08576f9e8a67f3789796a065837544f9f683ebf3188012fdbd", size = 3179818, upload-time = "2025-08-13T06:20:24.419Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/7b32a19c4c6bb0e2cc7ae52269b6b23a42f67f730e4abd4f8dca63660f7a/cython-3.1.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b827ce7d97ef8624adcf2bdda594b3dcb6c9b4f124d8f72001d8aea27d69dc1c", size = 3403206, upload-time = "2025-08-13T06:20:26.846Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e1/08cfd4c5e99f79e62d4a7b0009bbd906748633270935c8a55eee51fead76/cython-3.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7851107204085f4f02d0eb6b660ddcad2ce4846e8b7a1eaba724a0bd3cd68a6b", size = 3327837, upload-time = "2025-08-13T06:20:28.946Z" }, - { url = "https://files.pythonhosted.org/packages/23/1b/f3d384be86fa2a6e110b42f42bf97295a513197aaa5532f451c73bf5b48e/cython-3.1.3-cp312-cp312-win32.whl", hash = "sha256:ed20f1b45b2da5a4f8e71a80025bca1cdc96ba35820b0b17658a4a025be920b0", size = 2485990, upload-time = "2025-08-13T06:20:31.517Z" }, - { url = "https://files.pythonhosted.org/packages/de/f0/17cff75e3c141bda73d770f7412632f53e55778d3bfdadfeec4dd3a7d1d6/cython-3.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc4ca0f4dec55124cd79ddcfc555be1cbe0092cc99bcf1403621d17b9c6218bb", size = 2704002, upload-time = "2025-08-13T06:20:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/56/c8/46ac27096684f33e27dab749ef43c6b0119c6a0d852971eaefb73256dc4c/cython-3.1.3-py3-none-any.whl", hash = "sha256:d13025b34f72f77bf7f65c1cd628914763e6c285f4deb934314c922b91e6be5a", size = 1225725, upload-time = "2025-08-13T06:19:09.593Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ab/0a568bac7c4c052db4ae27edf01e16f3093cdfef04a2dfd313ef1b3c478a/cython-3.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d1d7013dba5fb0506794d4ef8947ff5ed021370614950a8d8d04e57c8c84499e", size = 3026389, upload-time = "2025-09-16T07:22:02.212Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b7/51f5566e1309215a7fef744975b2fabb56d3fdc5fa1922fd7e306c14f523/cython-3.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eed989f5c139d6550ef2665b783d86fab99372590c97f10a3c26c4523c5fce9e", size = 2955954, upload-time = "2025-09-16T07:22:03.782Z" }, + { url = "https://files.pythonhosted.org/packages/28/fd/ad8314520000fe96292fb8208c640fa862baa3053d2f3453a2acb50cafb8/cython-3.1.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df3beb8b024dfd73cfddb7f2f7456751cebf6e31655eed3189c209b634bc2f2", size = 3412005, upload-time = "2025-09-16T07:22:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3b/e570f8bcb392e7943fc9a25d1b2d1646ef0148ff017d3681511acf6bbfdc/cython-3.1.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8354703f1168e1aaa01348940f719734c1f11298be333bdb5b94101d49677c0", size = 3191100, upload-time = "2025-09-16T07:22:07.144Z" }, + { url = "https://files.pythonhosted.org/packages/78/81/f1ea09f563ebab732542cb11bf363710e53f3842458159ea2c160788bc8e/cython-3.1.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a928bd7d446247855f54f359057ab4a32c465219c8c1e299906a483393a59a9e", size = 3313786, upload-time = "2025-09-16T07:22:09.15Z" }, + { url = "https://files.pythonhosted.org/packages/ca/17/06575eb6175a926523bada7dac1cd05cc74add96cebbf2e8b492a2494291/cython-3.1.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c233bfff4cc7b9d629eecb7345f9b733437f76dc4441951ec393b0a6e29919fc", size = 3205775, upload-time = "2025-09-16T07:22:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/10/ba/61a8cf56a76ab21ddf6476b70884feff2a2e56b6d9010e1e1b1e06c46f70/cython-3.1.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e9691a2cbc2faf0cd819108bceccf9bfc56c15a06d172eafe74157388c44a601", size = 3428423, upload-time = "2025-09-16T07:22:12.404Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/42cf9239088d6b4b62c1c017c36e0e839f64c8d68674ce4172d0e0168d3b/cython-3.1.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ada319207432ea7c6691c70b5c112d261637d79d21ba086ae3726fedde79bfbf", size = 3330489, upload-time = "2025-09-16T07:22:14.576Z" }, + { url = "https://files.pythonhosted.org/packages/b5/08/36a619d6b1fc671a11744998e5cdd31790589e3cb4542927c97f3f351043/cython-3.1.4-cp311-cp311-win32.whl", hash = "sha256:dae81313c28222bf7be695f85ae1d16625aac35a0973a3af1e001f63379440c5", size = 2482410, upload-time = "2025-09-16T07:22:17.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/58/7d9ae7944bcd32e6f02d1a8d5d0c3875125227d050e235584127f2c64ffd/cython-3.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:60d2f192059ac34c5c26527f2beac823d34aaa766ef06792a3b7f290c18ac5e2", size = 2713755, upload-time = "2025-09-16T07:22:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/2939c739cfdc67ab94935a2c4fcc75638afd15e1954552655503a4112e92/cython-3.1.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d26af46505d0e54fe0f05e7ad089fd0eed8fa04f385f3ab88796f554467bcb9", size = 3062976, upload-time = "2025-09-16T07:22:20.517Z" }, + { url = "https://files.pythonhosted.org/packages/eb/bd/a84de57fd01017bf5dba84a49aeee826db21112282bf8d76ab97567ee15d/cython-3.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ac8bb5068156c92359e3f0eefa138c177d59d1a2e8a89467881fa7d06aba3b", size = 2970701, upload-time = "2025-09-16T07:22:22.644Z" }, + { url = "https://files.pythonhosted.org/packages/71/79/a09004c8e42f5be188c7636b1be479cdb244a6d8837e1878d062e4e20139/cython-3.1.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2e42714faec723d2305607a04bafb49a48a8d8f25dd39368d884c058dbcfbc", size = 3387730, upload-time = "2025-09-16T07:22:24.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bd/979f8c59e247f562642f3eb98a1b453530e1f7954ef071835c08ed2bf6ba/cython-3.1.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0fd655b27997a209a574873304ded9629de588f021154009e8f923475e2c677", size = 3167289, upload-time = "2025-09-16T07:22:26.35Z" }, + { url = "https://files.pythonhosted.org/packages/34/f8/0b98537f0b4e8c01f76d2a6cf75389987538e4d4ac9faf25836fd18c9689/cython-3.1.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9def7c41f4dc339003b1e6875f84edf059989b9c7f5e9a245d3ce12c190742d9", size = 3321099, upload-time = "2025-09-16T07:22:27.957Z" }, + { url = "https://files.pythonhosted.org/packages/f3/39/437968a2e7c7f57eb6e1144f6aca968aa15fbbf169b2d4da5d1ff6c21442/cython-3.1.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:196555584a8716bf7e017e23ca53e9f632ed493f9faa327d0718e7551588f55d", size = 3179897, upload-time = "2025-09-16T07:22:30.014Z" }, + { url = "https://files.pythonhosted.org/packages/2c/04/b3f42915f034d133f1a34e74a2270bc2def02786f9b40dc9028fbb968814/cython-3.1.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7fff0e739e07a20726484b8898b8628a7b87acb960d0fc5486013c6b77b7bb97", size = 3400936, upload-time = "2025-09-16T07:22:31.705Z" }, + { url = "https://files.pythonhosted.org/packages/21/eb/2ad9fa0896ab6cf29875a09a9f4aaea37c28b79b869a013bf9b58e4e652e/cython-3.1.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2754034fa10f95052949cd6b07eb2f61d654c1b9cfa0b17ea53a269389422e8", size = 3332131, upload-time = "2025-09-16T07:22:33.32Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bf/f19283f8405e7e564c3353302a8665ea2c589be63a8e1be1b503043366a9/cython-3.1.4-cp312-cp312-win32.whl", hash = "sha256:2e0808ff3614a1dbfd1adfcbff9b2b8119292f1824b3535b4a173205109509f8", size = 2487672, upload-time = "2025-09-16T07:22:35.227Z" }, + { url = "https://files.pythonhosted.org/packages/30/bf/32150a2e6c7b50b81c5dc9e942d41969400223a9c49d04e2ed955709894c/cython-3.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f262b32327b6bce340cce5d45bbfe3972cb62543a4930460d8564a489f3aea12", size = 2705348, upload-time = "2025-09-16T07:22:37.922Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/f7351052cf9db771fe4f32fca47fd66e6d9b53d8613b17faf7d130a9d553/cython-3.1.4-py3-none-any.whl", hash = "sha256:d194d95e4fa029a3f6c7d46bdd16d973808c7ea4797586911fdb67cb98b1a2c6", size = 1227541, upload-time = "2025-09-16T07:20:29.595Z" }, ] [[package]] @@ -477,11 +479,11 @@ wheels = [ [[package]] name = "dnspython" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] @@ -525,27 +527,27 @@ wheels = [ [[package]] name = "fonttools" -version = "4.59.2" +version = "4.60.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/a5/fba25f9fbdab96e26dedcaeeba125e5f05a09043bf888e0305326e55685b/fonttools-4.59.2.tar.gz", hash = "sha256:e72c0749b06113f50bcb80332364c6be83a9582d6e3db3fe0b280f996dc2ef22", size = 3540889, upload-time = "2025-08-27T16:40:30.97Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/d9/4eabd956fe123651a1f0efe29d9758b3837b5ae9a98934bdb571117033bb/fonttools-4.60.0.tar.gz", hash = "sha256:8f5927f049091a0ca74d35cce7f78e8f7775c83a6901a8fbe899babcc297146a", size = 3553671, upload-time = "2025-09-17T11:34:01.504Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/53/742fcd750ae0bdc74de4c0ff923111199cc2f90a4ee87aaddad505b6f477/fonttools-4.59.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:511946e8d7ea5c0d6c7a53c4cb3ee48eda9ab9797cd9bf5d95829a398400354f", size = 2774961, upload-time = "2025-08-27T16:38:47.536Z" }, - { url = "https://files.pythonhosted.org/packages/57/2a/976f5f9fa3b4dd911dc58d07358467bec20e813d933bc5d3db1a955dd456/fonttools-4.59.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5e2682cf7be766d84f462ba8828d01e00c8751a8e8e7ce12d7784ccb69a30d", size = 2344690, upload-time = "2025-08-27T16:38:49.723Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8f/b7eefc274fcf370911e292e95565c8253b0b87c82a53919ab3c795a4f50e/fonttools-4.59.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5729e12a982dba3eeae650de48b06f3b9ddb51e9aee2fcaf195b7d09a96250e2", size = 5026910, upload-time = "2025-08-27T16:38:51.904Z" }, - { url = "https://files.pythonhosted.org/packages/69/95/864726eaa8f9d4e053d0c462e64d5830ec7c599cbdf1db9e40f25ca3972e/fonttools-4.59.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c52694eae5d652361d59ecdb5a2246bff7cff13b6367a12da8499e9df56d148d", size = 4971031, upload-time = "2025-08-27T16:38:53.676Z" }, - { url = "https://files.pythonhosted.org/packages/24/4c/b8c4735ebdea20696277c70c79e0de615dbe477834e5a7c2569aa1db4033/fonttools-4.59.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f1bbc23ba1312bd8959896f46f667753b90216852d2a8cfa2d07e0cb234144", size = 5006112, upload-time = "2025-08-27T16:38:55.69Z" }, - { url = "https://files.pythonhosted.org/packages/3b/23/f9ea29c292aa2fc1ea381b2e5621ac436d5e3e0a5dee24ffe5404e58eae8/fonttools-4.59.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a1bfe5378962825dabe741720885e8b9ae9745ec7ecc4a5ec1f1ce59a6062bf", size = 5117671, upload-time = "2025-08-27T16:38:58.984Z" }, - { url = "https://files.pythonhosted.org/packages/ba/07/cfea304c555bf06e86071ff2a3916bc90f7c07ec85b23bab758d4908c33d/fonttools-4.59.2-cp311-cp311-win32.whl", hash = "sha256:e937790f3c2c18a1cbc7da101550a84319eb48023a715914477d2e7faeaba570", size = 2218157, upload-time = "2025-08-27T16:39:00.75Z" }, - { url = "https://files.pythonhosted.org/packages/d7/de/35d839aa69db737a3f9f3a45000ca24721834d40118652a5775d5eca8ebb/fonttools-4.59.2-cp311-cp311-win_amd64.whl", hash = "sha256:9836394e2f4ce5f9c0a7690ee93bd90aa1adc6b054f1a57b562c5d242c903104", size = 2265846, upload-time = "2025-08-27T16:39:02.453Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3d/1f45db2df51e7bfa55492e8f23f383d372200be3a0ded4bf56a92753dd1f/fonttools-4.59.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82906d002c349cad647a7634b004825a7335f8159d0d035ae89253b4abf6f3ea", size = 2769711, upload-time = "2025-08-27T16:39:04.423Z" }, - { url = "https://files.pythonhosted.org/packages/29/df/cd236ab32a8abfd11558f296e064424258db5edefd1279ffdbcfd4fd8b76/fonttools-4.59.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a10c1bd7644dc58f8862d8ba0cf9fb7fef0af01ea184ba6ce3f50ab7dfe74d5a", size = 2340225, upload-time = "2025-08-27T16:39:06.143Z" }, - { url = "https://files.pythonhosted.org/packages/98/12/b6f9f964fe6d4b4dd4406bcbd3328821c3de1f909ffc3ffa558fe72af48c/fonttools-4.59.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:738f31f23e0339785fd67652a94bc69ea49e413dfdb14dcb8c8ff383d249464e", size = 4912766, upload-time = "2025-08-27T16:39:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/78/82bde2f2d2c306ef3909b927363170b83df96171f74e0ccb47ad344563cd/fonttools-4.59.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ec99f9bdfee9cdb4a9172f9e8fd578cce5feb231f598909e0aecf5418da4f25", size = 4955178, upload-time = "2025-08-27T16:39:10.094Z" }, - { url = "https://files.pythonhosted.org/packages/92/77/7de766afe2d31dda8ee46d7e479f35c7d48747e558961489a2d6e3a02bd4/fonttools-4.59.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0476ea74161322e08c7a982f83558a2b81b491509984523a1a540baf8611cc31", size = 4897898, upload-time = "2025-08-27T16:39:12.087Z" }, - { url = "https://files.pythonhosted.org/packages/c5/77/ce0e0b905d62a06415fda9f2b2e109a24a5db54a59502b769e9e297d2242/fonttools-4.59.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95922a922daa1f77cc72611747c156cfb38030ead72436a2c551d30ecef519b9", size = 5049144, upload-time = "2025-08-27T16:39:13.84Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ea/870d93aefd23fff2e07cbeebdc332527868422a433c64062c09d4d5e7fe6/fonttools-4.59.2-cp312-cp312-win32.whl", hash = "sha256:39ad9612c6a622726a6a130e8ab15794558591f999673f1ee7d2f3d30f6a3e1c", size = 2206473, upload-time = "2025-08-27T16:39:15.854Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/e44bad000c4a4bb2e9ca11491d266e857df98ab6d7428441b173f0fe2517/fonttools-4.59.2-cp312-cp312-win_amd64.whl", hash = "sha256:980fd7388e461b19a881d35013fec32c713ffea1fc37aef2f77d11f332dfd7da", size = 2254706, upload-time = "2025-08-27T16:39:17.893Z" }, - { url = "https://files.pythonhosted.org/packages/65/a4/d2f7be3c86708912c02571db0b550121caab8cd88a3c0aacb9cfa15ea66e/fonttools-4.59.2-py3-none-any.whl", hash = "sha256:8bd0f759020e87bb5d323e6283914d9bf4ae35a7307dafb2cbd1e379e720ad37", size = 1132315, upload-time = "2025-08-27T16:40:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/da/3d/c57731fbbf204ef1045caca28d5176430161ead73cd9feac3e9d9ef77ee6/fonttools-4.60.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9106c202d68ff5f9b4a0094c4d7ad2eaa7e9280f06427b09643215e706eb016", size = 2830883, upload-time = "2025-09-17T11:32:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/b7a6ebaed464ce441c755252cc222af11edc651d17c8f26482f429cc2c0e/fonttools-4.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9da3a4a3f2485b156bb429b4f8faa972480fc01f553f7c8c80d05d48f17eec89", size = 2356005, upload-time = "2025-09-17T11:32:13.248Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c2/ea834e921324e2051403e125c1fe0bfbdde4951a7c1784e4ae6bdbd286cc/fonttools-4.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f84de764c6057b2ffd4feb50ddef481d92e348f0c70f2c849b723118d352bf3", size = 5041201, upload-time = "2025-09-17T11:32:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3c/1c64a338e9aa410d2d0728827d5bb1301463078cb225b94589f27558b427/fonttools-4.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800b3fa0d5c12ddff02179d45b035a23989a6c597a71c8035c010fff3b2ef1bb", size = 4977696, upload-time = "2025-09-17T11:32:17.674Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/c8c411a0d9732bb886b870e052f20658fec9cf91118314f253950d2c1d65/fonttools-4.60.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd68f60b030277f292a582d31c374edfadc60bb33d51ec7b6cd4304531819ba", size = 5020386, upload-time = "2025-09-17T11:32:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/1d3bc07cf92e7f4fc27f06d4494bf6078dc595b2e01b959157a4fd23df12/fonttools-4.60.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:53328e3ca9e5c8660ef6de07c35f8f312c189b757535e12141be7a8ec942de6e", size = 5131575, upload-time = "2025-09-17T11:32:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/5a/16/08db3917ee19e89d2eb0ee637d37cd4136c849dc421ff63f406b9165c1a1/fonttools-4.60.0-cp311-cp311-win32.whl", hash = "sha256:d493c175ddd0b88a5376e61163e3e6fde3be8b8987db9b092e0a84650709c9e7", size = 2229297, upload-time = "2025-09-17T11:32:24.834Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0b/76764da82c0dfcea144861f568d9e83f4b921e84f2be617b451257bb25a7/fonttools-4.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc2770c9dc49c2d0366e9683f4d03beb46c98042d7ccc8ddbadf3459ecb051a7", size = 2277193, upload-time = "2025-09-17T11:32:27.094Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9b/706ebf84b55ab03439c1f3a94d6915123c0d96099f4238b254fdacffe03a/fonttools-4.60.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8c68928a438d60dfde90e2f09aa7f848ed201176ca6652341744ceec4215859f", size = 2831953, upload-time = "2025-09-17T11:32:29.39Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/782f485be450846e4f3aecff1f10e42af414fc6e19d235c70020f64278e1/fonttools-4.60.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7133821249097cffabf0624eafd37f5a3358d5ce814febe9db688e3673e724e", size = 2351716, upload-time = "2025-09-17T11:32:31.46Z" }, + { url = "https://files.pythonhosted.org/packages/39/77/ad8d2a6ecc19716eb488c8cf118de10f7802e14bdf61d136d7b52358d6b1/fonttools-4.60.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3638905d3d77ac8791127ce181f7cb434f37e4204d8b2e31b8f1e154320b41f", size = 4922729, upload-time = "2025-09-17T11:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/6b/48/aa543037c6e7788e1bc36b3f858ac70a59d32d0f45915263d0b330a35140/fonttools-4.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7968a26ef010ae89aabbb2f8e9dec1e2709a2541bb8620790451ee8aeb4f6fbf", size = 4967188, upload-time = "2025-09-17T11:32:35.74Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/e407d2028adc6387947eff8f2940b31f4ed40b9a83c2c7bbc8b9255126e2/fonttools-4.60.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ef01ca7847c356b0fe026b7b92304bc31dc60a4218689ee0acc66652c1a36b2", size = 4910043, upload-time = "2025-09-17T11:32:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/16/ef/e78519b3c296ef757a21b792fc6a785aa2ef9a2efb098083d8ed5f6ee2ba/fonttools-4.60.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f3482d7ed7867edfcf785f77c1dffc876c4b2ddac19539c075712ff2a0703cf5", size = 5061980, upload-time = "2025-09-17T11:32:40.457Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/ad72444d1e3ef704ee90af8d5abf198016a39908d322bf41235562fb01a0/fonttools-4.60.0-cp312-cp312-win32.whl", hash = "sha256:8c937c4fe8addff575a984c9519433391180bf52cf35895524a07b520f376067", size = 2217750, upload-time = "2025-09-17T11:32:42.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/3e8ac21963e130242f5a9ea2ebc57f5726d704bf4dcca89088b5b637b2d3/fonttools-4.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:99b06d5d6f29f32e312adaed0367112f5ff2d300ea24363d377ec917daf9e8c5", size = 2266025, upload-time = "2025-09-17T11:32:44.8Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/247d3e54eb5ed59e94e09866cfc4f9567e274fbf310ba390711851f63b3b/fonttools-4.60.0-py3-none-any.whl", hash = "sha256:496d26e4d14dcccdd6ada2e937e4d174d3138e3d73f5c9b6ec6eb2fd1dab4f66", size = 1142186, upload-time = "2025-09-17T11:33:59.287Z" }, ] [[package]] @@ -737,11 +739,11 @@ wheels = [ [[package]] name = "kaitaistruct" -version = "0.10" +version = "0.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/04/dd60b9cb65d580ef6cb6eaee975ad1bdd22d46a3f51b07a1e0606710ea88/kaitaistruct-0.10.tar.gz", hash = "sha256:a044dee29173d6afbacf27bcac39daf89b654dd418cfa009ab82d9178a9ae52a", size = 7061, upload-time = "2022-07-09T00:34:06.729Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/b8/ca7319556912f68832daa4b81425314857ec08dfccd8dbc8c0f65c992108/kaitaistruct-0.11.tar.gz", hash = "sha256:053ee764288e78b8e53acf748e9733268acbd579b8d82a427b1805453625d74b", size = 11519, upload-time = "2025-09-08T15:46:25.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/bf/88ad23efc08708bda9a2647169828e3553bb2093a473801db61f75356395/kaitaistruct-0.10-py2.py3-none-any.whl", hash = "sha256:a97350919adbf37fda881f75e9365e2fb88d04832b7a4e57106ec70119efb235", size = 7013, upload-time = "2022-07-09T00:34:03.905Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/cf14bf3b1f5ffb13c69cf5f0ea78031247790558ee88984a8bdd22fae60d/kaitaistruct-0.11-py2.py3-none-any.whl", hash = "sha256:5c6ce79177b4e193a577ecd359e26516d1d6d000a0bffd6e1010f2a46a62a561", size = 11372, upload-time = "2025-09-08T15:46:23.635Z" }, ] [[package]] @@ -796,57 +798,82 @@ wheels = [ [[package]] name = "lxml" -version = "6.0.1" +version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/bd/f9d01fd4132d81c6f43ab01983caea69ec9614b913c290a26738431a015d/lxml-6.0.1.tar.gz", hash = "sha256:2b3a882ebf27dd026df3801a87cf49ff791336e0f94b0fad195db77e01240690", size = 4070214, upload-time = "2025-08-22T10:37:53.525Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/c8/262c1d19339ef644cdc9eb5aad2e85bd2d1fa2d7c71cdef3ede1a3eed84d/lxml-6.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6acde83f7a3d6399e6d83c1892a06ac9b14ea48332a5fbd55d60b9897b9570a", size = 8422719, upload-time = "2025-08-22T10:32:24.848Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d4/1b0afbeb801468a310642c3a6f6704e53c38a4a6eb1ca6faea013333e02f/lxml-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d21c9cacb6a889cbb8eeb46c77ef2c1dd529cde10443fdeb1de847b3193c541", size = 4575763, upload-time = "2025-08-22T10:32:27.057Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c1/8db9b5402bf52ceb758618313f7423cd54aea85679fcf607013707d854a8/lxml-6.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:847458b7cd0d04004895f1fb2cca8e7c0f8ec923c49c06b7a72ec2d48ea6aca2", size = 4943244, upload-time = "2025-08-22T10:32:28.847Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/838e115358dd2369c1c5186080dd874a50a691fb5cd80db6afe5e816e2c6/lxml-6.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1dc13405bf315d008fe02b1472d2a9d65ee1c73c0a06de5f5a45e6e404d9a1c0", size = 5081725, upload-time = "2025-08-22T10:32:30.666Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b6/bdcb3a3ddd2438c5b1a1915161f34e8c85c96dc574b0ef3be3924f36315c/lxml-6.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f540c229a8c0a770dcaf6d5af56a5295e0fc314fc7ef4399d543328054bcea", size = 5021238, upload-time = "2025-08-22T10:32:32.49Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/1bfb96185dc1a64c7c6fbb7369192bda4461952daa2025207715f9968205/lxml-6.0.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d2f73aef768c70e8deb8c4742fca4fd729b132fda68458518851c7735b55297e", size = 5343744, upload-time = "2025-08-22T10:32:34.385Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ae/df3ea9ebc3c493b9c6bdc6bd8c554ac4e147f8d7839993388aab57ec606d/lxml-6.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7f4066b85a4fa25ad31b75444bd578c3ebe6b8ed47237896341308e2ce923c3", size = 5223477, upload-time = "2025-08-22T10:32:36.256Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/65e1e33600542c08bc03a4c5c9c306c34696b0966a424a3be6ffec8038ed/lxml-6.0.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0cce65db0cd8c750a378639900d56f89f7d6af11cd5eda72fde054d27c54b8ce", size = 4676626, upload-time = "2025-08-22T10:32:38.793Z" }, - { url = "https://files.pythonhosted.org/packages/7a/46/ee3ed8f3a60e9457d7aea46542d419917d81dbfd5700fe64b2a36fb5ef61/lxml-6.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c372d42f3eee5844b69dcab7b8d18b2f449efd54b46ac76970d6e06b8e8d9a66", size = 5066042, upload-time = "2025-08-22T10:32:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b9/8394538e7cdbeb3bfa36bc74924be1a4383e0bb5af75f32713c2c4aa0479/lxml-6.0.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2e2b0e042e1408bbb1c5f3cfcb0f571ff4ac98d8e73f4bf37c5dd179276beedd", size = 4724714, upload-time = "2025-08-22T10:32:43.94Z" }, - { url = "https://files.pythonhosted.org/packages/b3/21/3ef7da1ea2a73976c1a5a311d7cde5d379234eec0968ee609517714940b4/lxml-6.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cc73bb8640eadd66d25c5a03175de6801f63c535f0f3cf50cac2f06a8211f420", size = 5247376, upload-time = "2025-08-22T10:32:46.263Z" }, - { url = "https://files.pythonhosted.org/packages/26/7d/0980016f124f00c572cba6f4243e13a8e80650843c66271ee692cddf25f3/lxml-6.0.1-cp311-cp311-win32.whl", hash = "sha256:7c23fd8c839708d368e406282d7953cee5134f4592ef4900026d84566d2b4c88", size = 3609499, upload-time = "2025-08-22T10:32:48.156Z" }, - { url = "https://files.pythonhosted.org/packages/b1/08/28440437521f265eff4413eb2a65efac269c4c7db5fd8449b586e75d8de2/lxml-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:2516acc6947ecd3c41a4a4564242a87c6786376989307284ddb115f6a99d927f", size = 4036003, upload-time = "2025-08-22T10:32:50.662Z" }, - { url = "https://files.pythonhosted.org/packages/7b/dc/617e67296d98099213a505d781f04804e7b12923ecd15a781a4ab9181992/lxml-6.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:cb46f8cfa1b0334b074f40c0ff94ce4d9a6755d492e6c116adb5f4a57fb6ad96", size = 3679662, upload-time = "2025-08-22T10:32:52.739Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a9/82b244c8198fcdf709532e39a1751943a36b3e800b420adc739d751e0299/lxml-6.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c03ac546adaabbe0b8e4a15d9ad815a281afc8d36249c246aecf1aaad7d6f200", size = 8422788, upload-time = "2025-08-22T10:32:56.612Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8d/1ed2bc20281b0e7ed3e6c12b0a16e64ae2065d99be075be119ba88486e6d/lxml-6.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33b862c7e3bbeb4ba2c96f3a039f925c640eeba9087a4dc7a572ec0f19d89392", size = 4593547, upload-time = "2025-08-22T10:32:59.016Z" }, - { url = "https://files.pythonhosted.org/packages/76/53/d7fd3af95b72a3493bf7fbe842a01e339d8f41567805cecfecd5c71aa5ee/lxml-6.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a3ec1373f7d3f519de595032d4dcafae396c29407cfd5073f42d267ba32440d", size = 4948101, upload-time = "2025-08-22T10:33:00.765Z" }, - { url = "https://files.pythonhosted.org/packages/9d/51/4e57cba4d55273c400fb63aefa2f0d08d15eac021432571a7eeefee67bed/lxml-6.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03b12214fb1608f4cffa181ec3d046c72f7e77c345d06222144744c122ded870", size = 5108090, upload-time = "2025-08-22T10:33:03.108Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6e/5f290bc26fcc642bc32942e903e833472271614e24d64ad28aaec09d5dae/lxml-6.0.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:207ae0d5f0f03b30f95e649a6fa22aa73f5825667fee9c7ec6854d30e19f2ed8", size = 5021791, upload-time = "2025-08-22T10:33:06.972Z" }, - { url = "https://files.pythonhosted.org/packages/13/d4/2e7551a86992ece4f9a0f6eebd4fb7e312d30f1e372760e2109e721d4ce6/lxml-6.0.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:32297b09ed4b17f7b3f448de87a92fb31bb8747496623483788e9f27c98c0f00", size = 5358861, upload-time = "2025-08-22T10:33:08.967Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/cb49d727fc388bf5fd37247209bab0da11697ddc5e976ccac4826599939e/lxml-6.0.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e18224ea241b657a157c85e9cac82c2b113ec90876e01e1f127312006233756", size = 5652569, upload-time = "2025-08-22T10:33:10.815Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b8/66c1ef8c87ad0f958b0a23998851e610607c74849e75e83955d5641272e6/lxml-6.0.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a07a994d3c46cd4020c1ea566345cf6815af205b1e948213a4f0f1d392182072", size = 5252262, upload-time = "2025-08-22T10:33:12.673Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ef/131d3d6b9590e64fdbb932fbc576b81fcc686289da19c7cb796257310e82/lxml-6.0.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:2287fadaa12418a813b05095485c286c47ea58155930cfbd98c590d25770e225", size = 4710309, upload-time = "2025-08-22T10:33:14.952Z" }, - { url = "https://files.pythonhosted.org/packages/bc/3f/07f48ae422dce44902309aa7ed386c35310929dc592439c403ec16ef9137/lxml-6.0.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4e597efca032ed99f418bd21314745522ab9fa95af33370dcee5533f7f70136", size = 5265786, upload-time = "2025-08-22T10:33:16.721Z" }, - { url = "https://files.pythonhosted.org/packages/11/c7/125315d7b14ab20d9155e8316f7d287a4956098f787c22d47560b74886c4/lxml-6.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9696d491f156226decdd95d9651c6786d43701e49f32bf23715c975539aa2b3b", size = 5062272, upload-time = "2025-08-22T10:33:18.478Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c3/51143c3a5fc5168a7c3ee626418468ff20d30f5a59597e7b156c1e61fba8/lxml-6.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e4e3cd3585f3c6f87cdea44cda68e692cc42a012f0131d25957ba4ce755241a7", size = 4786955, upload-time = "2025-08-22T10:33:20.34Z" }, - { url = "https://files.pythonhosted.org/packages/11/86/73102370a420ec4529647b31c4a8ce8c740c77af3a5fae7a7643212d6f6e/lxml-6.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:45cbc92f9d22c28cd3b97f8d07fcefa42e569fbd587dfdac76852b16a4924277", size = 5673557, upload-time = "2025-08-22T10:33:22.282Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2d/aad90afaec51029aef26ef773b8fd74a9e8706e5e2f46a57acd11a421c02/lxml-6.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f8c9bcfd2e12299a442fba94459adf0b0d001dbc68f1594439bfa10ad1ecb74b", size = 5254211, upload-time = "2025-08-22T10:33:24.15Z" }, - { url = "https://files.pythonhosted.org/packages/63/01/c9e42c8c2d8b41f4bdefa42ab05448852e439045f112903dd901b8fbea4d/lxml-6.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e9dc2b9f1586e7cd77753eae81f8d76220eed9b768f337dc83a3f675f2f0cf9", size = 5275817, upload-time = "2025-08-22T10:33:26.007Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1f/962ea2696759abe331c3b0e838bb17e92224f39c638c2068bf0d8345e913/lxml-6.0.1-cp312-cp312-win32.whl", hash = "sha256:987ad5c3941c64031f59c226167f55a04d1272e76b241bfafc968bdb778e07fb", size = 3610889, upload-time = "2025-08-22T10:33:28.169Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/22c86a990b51b44442b75c43ecb2f77b8daba8c4ba63696921966eac7022/lxml-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:abb05a45394fd76bf4a60c1b7bec0e6d4e8dfc569fc0e0b1f634cd983a006ddc", size = 4010925, upload-time = "2025-08-22T10:33:29.874Z" }, - { url = "https://files.pythonhosted.org/packages/b2/21/dc0c73325e5eb94ef9c9d60dbb5dcdcb2e7114901ea9509735614a74e75a/lxml-6.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:c4be29bce35020d8579d60aa0a4e95effd66fcfce31c46ffddf7e5422f73a299", size = 3671922, upload-time = "2025-08-22T10:33:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/41/37/41961f53f83ded57b37e65e4f47d1c6c6ef5fd02cb1d6ffe028ba0efa7d4/lxml-6.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b556aaa6ef393e989dac694b9c95761e32e058d5c4c11ddeef33f790518f7a5e", size = 3903412, upload-time = "2025-08-22T10:37:40.758Z" }, - { url = "https://files.pythonhosted.org/packages/3d/47/8631ea73f3dc776fb6517ccde4d5bd5072f35f9eacbba8c657caa4037a69/lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:64fac7a05ebb3737b79fd89fe5a5b6c5546aac35cfcfd9208eb6e5d13215771c", size = 4224810, upload-time = "2025-08-22T10:37:42.839Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b8/39ae30ca3b1516729faeef941ed84bf8f12321625f2644492ed8320cb254/lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:038d3c08babcfce9dc89aaf498e6da205efad5b7106c3b11830a488d4eadf56b", size = 4329221, upload-time = "2025-08-22T10:37:45.223Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ea/048dea6cdfc7a72d40ae8ed7e7d23cf4a6b6a6547b51b492a3be50af0e80/lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:445f2cee71c404ab4259bc21e20339a859f75383ba2d7fb97dfe7c163994287b", size = 4270228, upload-time = "2025-08-22T10:37:47.276Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d4/c2b46e432377c45d611ae2f669aa47971df1586c1a5240675801d0f02bac/lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e352d8578e83822d70bea88f3d08b9912528e4c338f04ab707207ab12f4b7aac", size = 4416077, upload-time = "2025-08-22T10:37:49.822Z" }, - { url = "https://files.pythonhosted.org/packages/b6/db/8f620f1ac62cf32554821b00b768dd5957ac8e3fd051593532be5b40b438/lxml-6.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:51bd5d1a9796ca253db6045ab45ca882c09c071deafffc22e06975b7ace36300", size = 3518127, upload-time = "2025-08-22T10:37:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "mapbox-earcut" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/70/0a322197c1178f47941e5e6e13b0a4adeaaa7c465c18e3b4ead3eba49860/mapbox_earcut-1.0.3.tar.gz", hash = "sha256:b6bac5d519d9947a6321a699c15d58e0b5740da61b9210ed229e05ad207c1c04", size = 24029, upload-time = "2024-12-25T12:49:09.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/d7/b37a45c248100e7285a40de87a8b1808ca4ca10228e265f2d0c320702d96/mapbox_earcut-1.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbf24029e7447eb0351000f4fd3185327a00dac5ed756b07330b0bdaed6932db", size = 71057, upload-time = "2024-12-25T12:48:09.131Z" }, + { url = "https://files.pythonhosted.org/packages/1b/df/2b63eb0d3a24e14f67adc816de18c2e09f3eb0997c512ace84dd59c3ed96/mapbox_earcut-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:998e2f1e3769538f7656a34296d08a37cb71ce57aa8cf4387572bc00029b52ce", size = 65300, upload-time = "2024-12-25T12:48:11.677Z" }, + { url = "https://files.pythonhosted.org/packages/87/37/9dd9575f5c00e35d480e7150e5bb315a35d9cf5642bfb75ca628a31e1341/mapbox_earcut-1.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2382d84d6d168f73479673d297753e37440772f233cc03ebb54d150e37b174", size = 96965, upload-time = "2024-12-25T12:48:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/3b/91/5708233941b5bf73149ba35f7aa32c6ee2cf4a33cd33069e7dba69d4129f/mapbox_earcut-1.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ccddb4bb04f11beab62943eb5a1bcd52c5a71d236bfce0ecc03e45e97fdb24b", size = 1070953, upload-time = "2024-12-25T12:48:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fe/b35b999ba786aa17ddc47bc04231de076665eb511e1cd58cf6fef3581172/mapbox_earcut-1.0.3-cp311-cp311-win32.whl", hash = "sha256:f19b2bcf6475bc591f48437d3214691a6730f39b1f6dfd7505b69c4345485b0c", size = 65245, upload-time = "2024-12-25T12:48:17.826Z" }, + { url = "https://files.pythonhosted.org/packages/11/81/18ac08b0bb0c22dd9028c7ecb31ae4086d31128b13fb3903e717331072ac/mapbox_earcut-1.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:811a64ad5e6ecf09b96af533e5c169299ba173e53eb4ff0209de1adcfae314be", size = 72356, upload-time = "2024-12-25T12:48:20.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/7c/707a4ce96e078f7d382cc32b4a6c2326eca68d77ead5e990f5f940d16140/mapbox_earcut-1.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5be71b7ec2180a27ce1178d53933430a3292b6ac3f94f2144513ee51d9034007", size = 70333, upload-time = "2024-12-25T12:48:22.565Z" }, + { url = "https://files.pythonhosted.org/packages/fb/47/ba2a14732f6e197b0ed879a1992b4d85054294b23627ad681b4fb1251d16/mapbox_earcut-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eb874f7562a49ae0fb7bd47bcc9b4854cc53e3e4f7f26674f02f3cadb006ce16", size = 64697, upload-time = "2024-12-25T12:48:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/e7/68/59a514811da76c3c801207bd6d7094ea5ba75648c2e7f15d4cb98b08216f/mapbox_earcut-1.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b9f06f2f8a795d835342aa80e021cfceda78fdca7bc07dc1a0b4aca90239f3", size = 96182, upload-time = "2024-12-25T12:48:26.316Z" }, + { url = "https://files.pythonhosted.org/packages/3f/79/97bf509ade0f9aeb5b5f94b1aff86393c2f584379a80e392fdfcbea434ae/mapbox_earcut-1.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdc55574ef7b613004874a459d2d59c07e1ef45cebb83f86c4958f7d3e2d6069", size = 1070584, upload-time = "2024-12-25T12:48:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/de/7a/5a6e205bab9ff49d1dae392f6179a444f820880d8985f26080816fa6c7ba/mapbox_earcut-1.0.3-cp312-cp312-win32.whl", hash = "sha256:790f52c67a0bd81032eaf61ebc181b1825b8b6daf01cb69e9eaa38521dd07aeb", size = 65375, upload-time = "2024-12-25T12:48:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/7a/59/674a67f92772563d5a943ce2c4ed834ed341e3a0fd77b8eb4b79057f5193/mapbox_earcut-1.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:cc1bbf35be0d9853dd448374330684ddbd0112497dee7d21b7417b0ab6236ac7", size = 72575, upload-time = "2024-12-25T12:48:33.544Z" }, ] [[package]] name = "markdown" -version = "3.8.2" +version = "3.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, ] [[package]] @@ -1128,28 +1155,28 @@ wheels = [ [[package]] name = "mypy" -version = "1.17.1" +version = "1.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, ] [[package]] @@ -1172,39 +1199,39 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.2" +version = "2.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158, upload-time = "2025-07-24T20:24:58.397Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817, upload-time = "2025-07-24T20:25:07.746Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606, upload-time = "2025-07-24T20:25:18.84Z" }, - { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652, upload-time = "2025-07-24T20:25:40.356Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816, upload-time = "2025-07-24T20:26:05.721Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512, upload-time = "2025-07-24T20:26:30.545Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947, upload-time = "2025-07-24T20:26:58.24Z" }, - { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494, upload-time = "2025-07-24T20:27:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889, upload-time = "2025-07-24T20:27:29.558Z" }, - { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560, upload-time = "2025-07-24T20:27:46.803Z" }, - { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, - { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, - { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, - { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, - { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338, upload-time = "2025-07-24T20:57:54.37Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776, upload-time = "2025-07-24T20:58:16.303Z" }, - { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882, upload-time = "2025-07-24T20:58:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405, upload-time = "2025-07-24T20:58:37.341Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651, upload-time = "2025-07-24T20:58:59.048Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166, upload-time = "2025-07-24T21:28:56.38Z" }, - { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/7a/45/e80d203ef6b267aa29b22714fb558930b27960a0c5ce3c19c999232bb3eb/numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", size = 21259253, upload-time = "2025-09-09T15:56:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/cf2c648fccf339e59302e00e5f2bc87725a3ce1992f30f3f78c9044d7c43/numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", size = 14450980, upload-time = "2025-09-09T15:56:05.926Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/9af1082bec870188c42a1c239839915b74a5099c392389ff04215dcee812/numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", size = 5379709, upload-time = "2025-09-09T15:56:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/bfd7abca52bcbf9a4a65abc83fe18ef01ccdeb37bfb28bbd6ad613447c79/numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", size = 6913923, upload-time = "2025-09-09T15:56:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/79/55/d69adad255e87ab7afda1caf93ca997859092afeb697703e2f010f7c2e55/numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", size = 14589591, upload-time = "2025-09-09T15:56:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/10/a2/010b0e27ddeacab7839957d7a8f00e91206e0c2c47abbb5f35a2630e5387/numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", size = 16938714, upload-time = "2025-09-09T15:56:14.637Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6b/12ce8ede632c7126eb2762b9e15e18e204b81725b81f35176eac14dc5b82/numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", size = 16370592, upload-time = "2025-09-09T15:56:17.285Z" }, + { url = "https://files.pythonhosted.org/packages/b4/35/aba8568b2593067bb6a8fe4c52babb23b4c3b9c80e1b49dff03a09925e4a/numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", size = 18884474, upload-time = "2025-09-09T15:56:20.943Z" }, + { url = "https://files.pythonhosted.org/packages/45/fa/7f43ba10c77575e8be7b0138d107e4f44ca4a1ef322cd16980ea3e8b8222/numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57", size = 6599794, upload-time = "2025-09-09T15:56:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a2/a4f78cb2241fe5664a22a10332f2be886dcdea8784c9f6a01c272da9b426/numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa", size = 13088104, upload-time = "2025-09-09T15:56:25.476Z" }, + { url = "https://files.pythonhosted.org/packages/79/64/e424e975adbd38282ebcd4891661965b78783de893b381cbc4832fb9beb2/numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7", size = 10460772, upload-time = "2025-09-09T15:56:27.679Z" }, + { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, + { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, + { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload-time = "2025-09-09T15:56:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload-time = "2025-09-09T15:56:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload-time = "2025-09-09T15:56:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/7e0a37cfced2644c9563c529f29fa28acbd0960dde32ece683aafa6f4949/numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", size = 21131019, upload-time = "2025-09-09T15:58:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/3291f505297ed63831135a6cc0f474da0c868a1f31b0dd9a9f03a7a0d2ed/numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", size = 14376288, upload-time = "2025-09-09T15:58:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4b/ae02e985bdeee73d7b5abdefeb98aef1207e96d4c0621ee0cf228ddfac3c/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", size = 5305425, upload-time = "2025-09-09T15:58:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/eb/9df215d6d7250db32007941500dc51c48190be25f2401d5b2b564e467247/numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", size = 6819053, upload-time = "2025-09-09T15:58:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/57/62/208293d7d6b2a8998a4a1f23ac758648c3c32182d4ce4346062018362e29/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", size = 14420354, upload-time = "2025-09-09T15:58:52.704Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0c/8e86e0ff7072e14a71b4c6af63175e40d1e7e933ce9b9e9f765a95b4e0c3/numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", size = 16760413, upload-time = "2025-09-09T15:58:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/11/0cc63f9f321ccf63886ac203336777140011fb669e739da36d8db3c53b98/numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", size = 12971844, upload-time = "2025-09-09T15:58:57.359Z" }, ] [[package]] @@ -1261,12 +1288,12 @@ dependencies = [ { name = "cffi" }, { name = "crcmod" }, { name = "cython" }, - { name = "dearpygui" }, { name = "future-fstrings" }, { name = "inputs" }, { name = "json-rpc" }, { name = "kaitaistruct" }, { name = "libusb1" }, + { name = "mapbox-earcut" }, { name = "numpy" }, { name = "onnx" }, { name = "psutil" }, @@ -1278,6 +1305,7 @@ dependencies = [ { name = "pyserial" }, { name = "pyzmq" }, { name = "qrcode" }, + { name = "raylib" }, { name = "requests" }, { name = "scons" }, { name = "sentry-sdk" }, @@ -1310,7 +1338,6 @@ dev = [ { name = "pyprof2calltree" }, { name = "pytools", marker = "platform_machine != 'aarch64'" }, { name = "pywinctl" }, - { name = "raylib" }, { name = "tabulate" }, { name = "types-requests" }, { name = "types-tabulate" }, @@ -1337,6 +1364,7 @@ testing = [ { name = "ruff" }, ] tools = [ + { name = "dearpygui" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, ] @@ -1353,7 +1381,7 @@ requires-dist = [ { name = "crcmod" }, { name = "cython" }, { name = "dbus-next", marker = "extra == 'dev'" }, - { name = "dearpygui", specifier = ">=2.1.0" }, + { name = "dearpygui", marker = "extra == 'tools'", specifier = ">=2.1.0" }, { name = "dictdiffer", marker = "extra == 'dev'" }, { name = "future-fstrings" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, @@ -1363,6 +1391,7 @@ requires-dist = [ { name = "json-rpc" }, { name = "kaitaistruct" }, { name = "libusb1" }, + { name = "mapbox-earcut" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, { name = "mkdocs", marker = "extra == 'docs'" }, @@ -1376,7 +1405,7 @@ requires-dist = [ { name = "psutil" }, { name = "pyaudio" }, { name = "pyautogui", marker = "extra == 'dev'" }, - { name = "pycapnp" }, + { name = "pycapnp", specifier = "==2.1.0" }, { name = "pycryptodome" }, { name = "pygame", marker = "extra == 'dev'" }, { name = "pyjwt" }, @@ -1397,7 +1426,7 @@ requires-dist = [ { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, { name = "qrcode" }, - { name = "raylib", marker = "extra == 'dev'" }, + { name = "raylib", specifier = "<5.5.0.3" }, { name = "requests" }, { name = "ruff", marker = "extra == 'testing'" }, { name = "scons" }, @@ -1605,31 +1634,32 @@ wheels = [ [[package]] name = "protobuf" -version = "6.32.0" +version = "6.32.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/df/fb4a8eeea482eca989b51cffd274aac2ee24e825f0bf3cbce5281fa1567b/protobuf-6.32.0.tar.gz", hash = "sha256:a81439049127067fc49ec1d36e25c6ee1d1a2b7be930675f919258d03c04e7d2", size = 440614, upload-time = "2025-08-14T21:21:25.015Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/18/df8c87da2e47f4f1dcc5153a81cd6bca4e429803f4069a299e236e4dd510/protobuf-6.32.0-cp310-abi3-win32.whl", hash = "sha256:84f9e3c1ff6fb0308dbacb0950d8aa90694b0d0ee68e75719cb044b7078fe741", size = 424409, upload-time = "2025-08-14T21:21:12.366Z" }, - { url = "https://files.pythonhosted.org/packages/e1/59/0a820b7310f8139bd8d5a9388e6a38e1786d179d6f33998448609296c229/protobuf-6.32.0-cp310-abi3-win_amd64.whl", hash = "sha256:a8bdbb2f009cfc22a36d031f22a625a38b615b5e19e558a7b756b3279723e68e", size = 435735, upload-time = "2025-08-14T21:21:15.046Z" }, - { url = "https://files.pythonhosted.org/packages/cc/5b/0d421533c59c789e9c9894683efac582c06246bf24bb26b753b149bd88e4/protobuf-6.32.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d52691e5bee6c860fff9a1c86ad26a13afbeb4b168cd4445c922b7e2cf85aaf0", size = 426449, upload-time = "2025-08-14T21:21:16.687Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7b/607764ebe6c7a23dcee06e054fd1de3d5841b7648a90fd6def9a3bb58c5e/protobuf-6.32.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:501fe6372fd1c8ea2a30b4d9be8f87955a64d6be9c88a973996cef5ef6f0abf1", size = 322869, upload-time = "2025-08-14T21:21:18.282Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/2e730bd1c25392fc32e3268e02446f0d77cb51a2c3a8486b1798e34d5805/protobuf-6.32.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:75a2aab2bd1aeb1f5dc7c5f33bcb11d82ea8c055c9becbb41c26a8c43fd7092c", size = 322009, upload-time = "2025-08-14T21:21:19.893Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f2/80ffc4677aac1bc3519b26bc7f7f5de7fce0ee2f7e36e59e27d8beb32dd1/protobuf-6.32.0-py3-none-any.whl", hash = "sha256:ba377e5b67b908c8f3072a57b63e2c6a4cbd18aea4ed98d2584350dbf46f2783", size = 169287, upload-time = "2025-08-14T21:21:23.515Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, ] [[package]] name = "psutil" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660, upload-time = "2025-09-17T20:14:52.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13", size = 245242, upload-time = "2025-09-17T20:14:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5", size = 246682, upload-time = "2025-09-17T20:14:58.25Z" }, + { url = "https://files.pythonhosted.org/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3", size = 287994, upload-time = "2025-09-17T20:14:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3", size = 291163, upload-time = "2025-09-17T20:15:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d", size = 293625, upload-time = "2025-09-17T20:15:04.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812, upload-time = "2025-09-17T20:15:07.462Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965, upload-time = "2025-09-17T20:15:09.673Z" }, + { url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" }, ] [[package]] @@ -1662,47 +1692,43 @@ sdist = { url = "https://files.pythonhosted.org/packages/65/ff/cdae0a8c2118a0de7 [[package]] name = "pycapnp" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/fb/54b46b52c1fa2acd9afd81bd05810c61bb1b05c6084c9625b64bc6d41843/pycapnp-2.0.0.tar.gz", hash = "sha256:503ab9b7b16773590ee226f2460408972c6b1c2cb2d819037115b919bef682be", size = 574848, upload-time = "2024-04-12T15:35:44.019Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/86/a57e3c92acd3e1d2fc3dcad683ada191f722e4ac927e1a384b228ec2780a/pycapnp-2.1.0.tar.gz", hash = "sha256:69cc3d861fee1c9b26c73ad2e8a5d51e76ad87e4ff9be33a4fd2fc72f5846aec", size = 689734, upload-time = "2025-09-05T03:50:40.851Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/82/cf311b1a9800b605759a38a0c337a55a639b685427364294e98a0f9b7306/pycapnp-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:829c7eb4e5f23dbcac25466110faf72a691035cf87c5d46e5053da15790e428d", size = 1673673, upload-time = "2024-04-12T15:33:32.211Z" }, - { url = "https://files.pythonhosted.org/packages/ae/55/4c03ca95c568776a1f637db9ffdcf302fb63f46e4d2a4f13edd8cb1a5f90/pycapnp-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dab60fbe3e4eaf99ec97918a0a776216c6c149b6d49261383d91c2201adb475d", size = 1513351, upload-time = "2024-04-12T15:33:35.156Z" }, - { url = "https://files.pythonhosted.org/packages/55/98/e4b2dea076f8a2575abc45cd879a91bc9aa975c69ae2ac1cab61d83c5087/pycapnp-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c48a0582078bb74d7326d28571db0b8e6919563365537a5a13e8f5360c12bfc", size = 4910666, upload-time = "2024-04-12T15:33:37.798Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ee/3b5a182588f89074f4002fa6247e3f963bb85bc808acd1ac8deed91f1fa7/pycapnp-2.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb5ab54aff857e3711d2c0cc934194aaffacdeb3481daa56863daef07d27941", size = 5007434, upload-time = "2024-04-12T15:33:40.362Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e9/515a2ca7fdc84d57c654280d0b71dfd782fd1773d384c0ec0d56dc6fc35b/pycapnp-2.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9600778036e6fe9dbea68f0c37678c5f4d561d2f2306b3cb741de5e1670ef2ae", size = 5188923, upload-time = "2024-04-12T15:33:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/5db346e238985a526ba7589ed24f92195dad39e7dec9d85b17a567600b6f/pycapnp-2.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7278ba0262fab8c398e77d634ae7ba026866d44b52cbfc27262be8d396ecacd1", size = 5048105, upload-time = "2024-04-12T15:33:44.294Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ff/02b4a87c9ff9793f26d8f3d95312d902d260c094f216d84e19528a506606/pycapnp-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23b2458d43c82302980a96518c96df257429204d2cc02bfff0c8cb6ebb371e01", size = 5063172, upload-time = "2024-04-12T15:33:46.954Z" }, - { url = "https://files.pythonhosted.org/packages/10/a1/35a7e14d765f99cfdcdfdcebc69bdf382f27016944470daa7a03c557f681/pycapnp-2.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd7755cc3fedc2ad8cc7864a0729471ddeff10c184963fe0f3689e295130f1b2", size = 5408718, upload-time = "2024-04-12T15:33:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/5c/59/8bc8a993c38808c6fd90b10becba8de4a54543e8441bd87ce44ef3b7eee4/pycapnp-2.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d47baf6b3db9981625ffc5ff188e089f2ebca8e7e1afb97aa5eb7bebb7bf3650", size = 5596714, upload-time = "2024-04-12T15:33:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7d/79c481ef77f29e81355e92bb250f0d2a37a76f5fe0ba9433bf6c6c88b6e4/pycapnp-2.0.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b375be92d93fdb6f7ac127ea9390bcec0fed4e485db137b084f9e7114dde7c83", size = 5709896, upload-time = "2024-04-12T15:33:53.716Z" }, - { url = "https://files.pythonhosted.org/packages/59/8d/f2eceeea1e8cae8b8a70a4752af5b772916f455e2ed388d0887e2b57d080/pycapnp-2.0.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:959bfdf1cddb3e5528e2293c4a375382be9a1bf044b073bc2e7eca1eb6b3a9a2", size = 5594823, upload-time = "2024-04-12T15:33:55.573Z" }, - { url = "https://files.pythonhosted.org/packages/2c/86/f8284637b61f83232e5618dd561a66080dd98ce2272d7e3ae89335d4fd97/pycapnp-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d873af167cf5cc7578ce5432eefcb442f866c8f7a6c57d188baf8c5e709fa39d", size = 5572564, upload-time = "2024-04-12T15:33:59.112Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b2/7f99d28a9935d1e37ec6955922c57b2be24fe0b74fe25929643686cc11e5/pycapnp-2.0.0-cp311-cp311-win32.whl", hash = "sha256:40ca8018e0b7686d549b920f087049b92a3e6f06976d9f5a8112603fc560cac4", size = 1040268, upload-time = "2024-04-12T15:34:00.933Z" }, - { url = "https://files.pythonhosted.org/packages/1d/37/89ab98961f18cffeae20d98cfc24afcfa85024bc014ecc48b0c4ac264fe0/pycapnp-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:d15cd8e46d541a899c84809095d7d7b3951f43642d1859e7a39bd91910778479", size = 1141758, upload-time = "2024-04-12T15:34:02.607Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d5/0ee84de3ce34a86c373b6cfbea17d5486c2ca942d51efa99a0069723c1e3/pycapnp-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0c111ef96676df25b8afef98f369d45f838ad4434e2898e48199eb43ef704efe", size = 1645816, upload-time = "2024-04-12T15:34:04.428Z" }, - { url = "https://files.pythonhosted.org/packages/35/1e/580572083165ba791fac5ae2d8917facb94db6e3f0500421673f55165dac/pycapnp-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d18906eb1fd1b9f206d93a9591ceedce1d52e7766b66e68f271453f104e9dca", size = 1507892, upload-time = "2024-04-12T15:34:06.933Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ed/46b3cc5d32c525b6a3acb67eb43de2cec692a62775ec1ab66dafe2b7d6ad/pycapnp-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5d1ed365ab1beabb8838068907a7190cc0b6f16de3499d783627e670fcc0eb2", size = 4707960, upload-time = "2024-04-12T15:34:08.771Z" }, - { url = "https://files.pythonhosted.org/packages/8e/51/0a0a4d4e44138adb84959478ea4966196c5ad32022f768b9b64d1590cb3e/pycapnp-2.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:495b39a7aa2629931bbca27ad743ce591c6c41e8f81792276be424742d9cd1c1", size = 4791780, upload-time = "2024-04-12T15:34:10.863Z" }, - { url = "https://files.pythonhosted.org/packages/28/71/2b59c6ddb253b25b3d01ee6f7b32b0297ac205c7272beeb6d13399054430/pycapnp-2.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50e814fbde072dcc3d868b5b5cbb9b7a66a70bff9ad03942f3be9baf3ca1cfc6", size = 4961068, upload-time = "2024-04-12T15:34:13.543Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b8/b64fdefa59d6d2802b5ee0a9439396c23a3e5954da6909be81f2722a234c/pycapnp-2.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:920fdda62d5fdef7a48339104dff0ceb9dcc21b138491f854457ba3a3d4d63ec", size = 4872917, upload-time = "2024-04-12T15:34:15.636Z" }, - { url = "https://files.pythonhosted.org/packages/c8/55/867595f575eb6cb3662e9a0b50a24b4be42df86f2938003e586f6c81606f/pycapnp-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f9142eb4714c152b09dda0b055ea9dd43fd8fd894132e7eb4fa235fb4915edd", size = 4912169, upload-time = "2024-04-12T15:34:17.758Z" }, - { url = "https://files.pythonhosted.org/packages/e4/11/0d36b45e5005ecdf8510081d16c6fb7b22b49651f64af36d138df97980cc/pycapnp-2.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c98f1d0c4d32109d03e42828ce3c65236afc895033633cbed3ca092993702e7b", size = 5201744, upload-time = "2024-04-12T15:34:20.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/29/ad1357998656b7141939e55bb3aea727c7a5478026feed7f8ee8cf52c935/pycapnp-2.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4d3250c1875a309d67551843cd8bf3c5e7fccf159b7f5c118a92aee36c0e871c", size = 5351113, upload-time = "2024-04-12T15:34:23.173Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b1/f4c442907948a29b6427dd7436f31d3732bb0d77f5c1dbcad749ba56dac0/pycapnp-2.0.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:174e6babe01f5507111c0ed226cd0b5e9325a9d2850751cfe4a57c1670f13881", size = 5472055, upload-time = "2024-04-12T15:34:25.799Z" }, - { url = "https://files.pythonhosted.org/packages/c1/06/a6eceb8b8015f518c0ccae1de5d1a6e18ed73b62b4b111aff54ce5f4f566/pycapnp-2.0.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:ed38ece414341285695526792e020f391f29f5064b2126d0367c8bdeef28e3e9", size = 5395743, upload-time = "2024-04-12T15:34:28.134Z" }, - { url = "https://files.pythonhosted.org/packages/e7/b0/63f2b0327853ae08158de61b4dfc7fa43ae5a5c00f1d28f769e7c30cdf55/pycapnp-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8a20b7dc55ef83a1fa446bf12680bce25caeb8f81788b623b072c3ec820db50d", size = 5405076, upload-time = "2024-04-12T15:34:30.305Z" }, - { url = "https://files.pythonhosted.org/packages/7d/24/e025dd95f1abf34e373fbab8841ac8e5fa62afe3af4a4b0c61bd01354400/pycapnp-2.0.0-cp312-cp312-win32.whl", hash = "sha256:145eea66233fb5ac9152cd1c06b999ddb691815126f87f5cc37b9cda5d569f8a", size = 1030361, upload-time = "2024-04-12T15:34:32.25Z" }, - { url = "https://files.pythonhosted.org/packages/3f/70/a71108ee9d4db9a027b665a2c383202407207174f1956195d5be45aca705/pycapnp-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:b8b03000769b29b36a8810f458b931f0f706f42027ee6676821eff28092d7734", size = 1135121, upload-time = "2024-04-12T15:34:34.208Z" }, + { url = "https://files.pythonhosted.org/packages/68/7c/934750a0ca77431a22e68e11521dcc6b801bea3ff37331d6a519e5ad142e/pycapnp-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efacc439ec287d9e8a0ebf01a515404eff795659401e65ba6f1819c7b24f4380", size = 1628855, upload-time = "2025-09-05T03:48:32.317Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a2/fd2c10b3f2e5010c747aa946b27fe09f665d65d5dc2afdd31838a3ef2f5d/pycapnp-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3d8af535a8b44dfd71731a191386c6b821b8a4915806948893d18c79f547a8e", size = 1496942, upload-time = "2025-09-05T03:48:34.905Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8a/42bd0e4c094ef534ac6890d34adae580cbbf5b0497fc0a6340bea833a617/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:117d1d5ebfc08cc189aca4f771b34fedc1291a3f9417167bd2d9b2a4e607e640", size = 5200170, upload-time = "2025-09-05T03:48:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/2e92268383135082191c3dea4a9ad184d20b7fb2dda1477fd6ee520fd88e/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:d881ccc69e381863a88c7b6c7092a6baecb6dfc8c5558d66bc967c7f778fe7bc", size = 5684026, upload-time = "2025-09-05T03:48:38.063Z" }, + { url = "https://files.pythonhosted.org/packages/46/9c/bca1cbd7711c9c0f0f62ca95a49835369a61c4f6527a6900c8982045bf2f/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:8a4ea330e38ba83f6f03fbdc1f58642eb53e6f6f66734a426fa592dc988d70e9", size = 5709307, upload-time = "2025-09-05T03:48:40.127Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/cd14676d992c7b166baa7e022b369c15240d408b202410d105b23b25f737/pycapnp-2.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb2563de4619d359820de9d47b4704e4f7eda193ffc4a56e39cdcd2c8301c336", size = 5386505, upload-time = "2025-09-05T03:48:41.785Z" }, + { url = "https://files.pythonhosted.org/packages/ae/dd/2fc57cebe9be7e4cd3d6aec0b9c8a0db9772c1b17c37cfe4f04c050422cf/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5265d1ae34f9c089fa6983f6c1be404ce480c82b927017290bd703328fa3f5df", size = 6095180, upload-time = "2025-09-05T03:48:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/5a/16/da8c1ada7770a532c859df475533eec5a1b2f5e81a269466a2fe670c5747/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b0a56370a868f752375a785bfb7e06b55cbe71605972615d1220c380bc452380", size = 6603414, upload-time = "2025-09-05T03:48:45.457Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/a36eacaf2da6a5ac9c6565600e559edf95115ff990aa3379aee8dd7ba4fe/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5d7403c25275cf4badf6f9d0c07b1cb94fcdd599df81aba9b621c32b3dcefae9", size = 6621440, upload-time = "2025-09-05T03:48:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/81/54/9150c03638cf4ecdf1664867382d0049146c658d6de30f189817c502df1a/pycapnp-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dea5d0d250fe4851b42cd380a207d773ebae76a990e542a888a5f1442f4c247e", size = 6354219, upload-time = "2025-09-05T03:48:49.336Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/e49ba2d74456d53b570c8d30a660c3b29ecfea075d5dd663132ff9049f19/pycapnp-2.1.0-cp311-cp311-win32.whl", hash = "sha256:593844c3cd92937eb5e7cd47ea3a62cde2d49a1fc05dba644f513c68f60f1318", size = 1053647, upload-time = "2025-09-05T03:48:51.108Z" }, + { url = "https://files.pythonhosted.org/packages/53/de/2b61908dc6abf25b17fed6b5a3b42a2226ec09467a3944f1d845ac29ef9b/pycapnp-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac13dd30062bb9985ae9ec4feca106af2b4fdac6468a09c7b74ad754f3921a06", size = 1208911, upload-time = "2025-09-05T03:48:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/74/0e/66b41ba600e5f2523e900b7cc0d2e8496b397a1f2d6a5b7b323ab83418b7/pycapnp-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d2ec561bc948d11f64f43bf9601bede5d6a603d105ae311bd5583c7130624a4", size = 1619223, upload-time = "2025-09-05T03:48:54.64Z" }, + { url = "https://files.pythonhosted.org/packages/40/6e/9bcb30180bd40cb0534124ff7f8ba8746a735018d593f608bf40c97821c0/pycapnp-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132cd97f57f6b6636323ca9b68d389dd90b96e87af38cde31e2b5c5a064f277e", size = 1484321, upload-time = "2025-09-05T03:48:55.85Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/9ee1c9ecaff499e4fd1df2f0335bc20f666ec6ce5cd80f8ab055007f3c9b/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:568e79268ba7c02a71fe558a8aec1ae3c0f0e6aff809ff618a46afe4964957d2", size = 5143502, upload-time = "2025-09-05T03:48:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/4d/50/65837e1416f7a8861ca1e8fe4582a5aef37192d7ef5e2ecfe46880bfdf9c/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:bcbf6f882d78d368c8e4bb792295392f5c4d71ddffa13a48da27e7bd47b99e37", size = 5508134, upload-time = "2025-09-05T03:48:59.383Z" }, + { url = "https://files.pythonhosted.org/packages/a1/59/46df6db800e77dbc3cc940723fb3fd7bc837327c858edf464a0f904bf547/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:dc25b96e393410dde25c61c1df3ce644700ef94826c829426d58c2c6b3e2d2f5", size = 5631794, upload-time = "2025-09-05T03:49:03.511Z" }, + { url = "https://files.pythonhosted.org/packages/63/9d/18e978500d5f6bd8d152f4d6919e3cfb83ead8a71c14613bbb54322df8b9/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:48938e0436ab1be615fc0a41434119a2065490a6212b9a5e56949e89b0588b76", size = 5369378, upload-time = "2025-09-05T03:49:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/96/dc/726f1917e9996dc29f9fd1cf30674a14546cdbdfa0777e1982b6bd1ad628/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c20de0f6e0b3fa9fa1df3864cf46051db3511b63bc29514d1092af65f2b82a0", size = 5999140, upload-time = "2025-09-05T03:49:07.341Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3a/3bbc4c5776fc32fbf8a59df5c7c5810efd292b933cd6545eb4b16d896268/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:18caca6527862475167c10ea0809531130585aa8a86cc76cd1629eb87ee30637", size = 6454308, upload-time = "2025-09-05T03:49:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/bf/dd/17e2d7808424f10ffddc47329b980488ed83ec716c504791787e593a7a93/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9dcc11237697007b66e3bfc500d2ad892bd79672c9b50d61fbf728c6aaf936de", size = 6544212, upload-time = "2025-09-05T03:49:10.675Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/68090013128d7853f34c43828dd4dc80a7c8516fd1b56057b134e1e4c2c0/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c151edf78155b6416e7cb31e2e333d302d742ba52bb37d4dbdf71e75cc999d46", size = 6295279, upload-time = "2025-09-05T03:49:12.712Z" }, + { url = "https://files.pythonhosted.org/packages/5b/52/7d85212b4fcf127588888f71d3dbf5558ee7dc302eba760b12b1b325f9a3/pycapnp-2.1.0-cp312-cp312-win32.whl", hash = "sha256:c09b28419321dafafc644d60c57ff8ccaf3c3e686801b6060c612a7a3c580944", size = 1038995, upload-time = "2025-09-05T03:49:14.165Z" }, + { url = "https://files.pythonhosted.org/packages/f2/12/25d283ebf5c28717364647672e7494dc46196ca7a662f5420e4866f45687/pycapnp-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:560cb69cc02b0347e85b0629e4c2f0a316240900aa905392f9df6bab0a359989", size = 1176620, upload-time = "2025-09-05T03:49:15.545Z" }, ] [[package]] name = "pycparser" -version = "2.22" +version = "2.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] @@ -1828,9 +1854,12 @@ wheels = [ [[package]] name = "pymsgbox" -version = "1.0.9" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ff/4c6f31a4f08979f12a663f2aeb6c8b765d3bd592e66eaaac445f547bb875/PyMsgBox-1.0.9.tar.gz", hash = "sha256:2194227de8bff7a3d6da541848705a155dcbb2a06ee120d9f280a1d7f51263ff", size = 18829, upload-time = "2020-10-11T01:51:43.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/6a/e80da7594ee598a776972d09e2813df2b06b3bc29218f440631dfa7c78a8/pymsgbox-2.0.1.tar.gz", hash = "sha256:98d055c49a511dcc10fa08c3043e7102d468f5e4b3a83c6d3c61df722c7d798d", size = 20768, upload-time = "2025-09-09T00:38:56.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/3e/08c8cac81b2b2f7502746e6b9c8e5b0ec6432cd882c605560fc409aaf087/pymsgbox-2.0.1-py3-none-any.whl", hash = "sha256:5de8ec19bca2ca7e6c09d39c817c83f17c75cee80275235f43a9931db699f73b", size = 9994, upload-time = "2025-09-09T00:38:55.672Z" }, +] [[package]] name = "pyobjc" @@ -4233,18 +4262,21 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.2.3" +version = "3.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] [[package]] name = "pyperclip" -version = "1.9.0" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/99/25f4898cf420efb6f45f519de018f4faea5391114a8618b16736ef3029f1/pyperclip-1.10.0.tar.gz", hash = "sha256:180c8346b1186921c75dfd14d9048a6b5d46bfc499778811952c6dd6eb1ca6be", size = 12193, upload-time = "2025-09-18T00:54:00.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/bc/22540e73c5f5ae18f02924cd3954a6c9a4aa6b713c841a94c98335d333a1/pyperclip-1.10.0-py3-none-any.whl", hash = "sha256:596fbe55dc59263bff26e61d2afbe10223e2fccb5210c9c96a28d6887cfcc7ec", size = 11062, upload-time = "2025-09-18T00:53:59.252Z" }, +] [[package]] name = "pyprof2calltree" @@ -4278,7 +4310,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -4287,21 +4319,22 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] [[package]] @@ -4318,26 +4351,26 @@ wheels = [ [[package]] name = "pytest-mock" -version = "3.14.1" +version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] name = "pytest-randomly" -version = "3.16.0" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367, upload-time = "2024-10-25T15:45:34.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1d/258a4bf1109258c00c35043f40433be5c16647387b6e7cd5582d638c116b/pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8", size = 14130, upload-time = "2025-09-12T15:23:00.085Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396, upload-time = "2024-10-25T15:45:32.78Z" }, + { url = "https://files.pythonhosted.org/packages/33/3e/a4a9227807b56869790aad3e24472a554b585974fe7e551ea350f50897ae/pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7", size = 8304, upload-time = "2025-09-12T15:22:58.946Z" }, ] [[package]] @@ -4379,7 +4412,7 @@ wheels = [ [[package]] name = "pytest-xdist" -version = "3.7.1.dev24+g2b4372b" +version = "3.7.1.dev24+g2b4372bd6" source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" } dependencies = [ { name = "execnet" }, @@ -4521,38 +4554,38 @@ wheels = [ [[package]] name = "pyzmq" -version = "27.0.2" +version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/66/159f38d184f08b5f971b467f87b1ab142ab1320d5200825c824b32b84b66/pyzmq-27.0.2.tar.gz", hash = "sha256:b398dd713b18de89730447347e96a0240225e154db56e35b6bb8447ffdb07798", size = 281440, upload-time = "2025-08-21T04:23:26.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/73/034429ab0f4316bf433eb6c20c3f49d1dc13b2ed4e4d951b283d300a0f35/pyzmq-27.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:063845960df76599ad4fad69fa4d884b3ba38304272104fdcd7e3af33faeeb1d", size = 1333169, upload-time = "2025-08-21T04:21:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/35/02/c42b3b526eb03a570c889eea85a5602797f800a50ba8b09ddbf7db568b78/pyzmq-27.0.2-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:845a35fb21b88786aeb38af8b271d41ab0967985410f35411a27eebdc578a076", size = 909176, upload-time = "2025-08-21T04:21:13.835Z" }, - { url = "https://files.pythonhosted.org/packages/1b/35/a1c0b988fabbdf2dc5fe94b7c2bcfd61e3533e5109297b8e0daf1d7a8d2d/pyzmq-27.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:515d20b5c3c86db95503faa989853a8ab692aab1e5336db011cd6d35626c4cb1", size = 668972, upload-time = "2025-08-21T04:21:15.315Z" }, - { url = "https://files.pythonhosted.org/packages/a0/63/908ac865da32ceaeecea72adceadad28ca25b23a2ca5ff018e5bff30116f/pyzmq-27.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:862aedec0b0684a5050cdb5ec13c2da96d2f8dffda48657ed35e312a4e31553b", size = 856962, upload-time = "2025-08-21T04:21:16.652Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5a/90b3cc20b65cdf9391896fcfc15d8db21182eab810b7ea05a2986912fbe2/pyzmq-27.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5bcfc51c7a4fce335d3bc974fd1d6a916abbcdd2b25f6e89d37b8def25f57", size = 1657712, upload-time = "2025-08-21T04:21:18.666Z" }, - { url = "https://files.pythonhosted.org/packages/c4/3c/32a5a80f9be4759325b8d7b22ce674bb87e586b4c80c6a9d77598b60d6f0/pyzmq-27.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38ff75b2a36e3a032e9fef29a5871e3e1301a37464e09ba364e3c3193f62982a", size = 2035054, upload-time = "2025-08-21T04:21:20.073Z" }, - { url = "https://files.pythonhosted.org/packages/13/61/71084fe2ff2d7dc5713f8740d735336e87544845dae1207a8e2e16d9af90/pyzmq-27.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a5709abe8d23ca158a9d0a18c037f4193f5b6afeb53be37173a41e9fb885792", size = 1894010, upload-time = "2025-08-21T04:21:21.96Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6b/77169cfb13b696e50112ca496b2ed23c4b7d8860a1ec0ff3e4b9f9926221/pyzmq-27.0.2-cp311-cp311-win32.whl", hash = "sha256:47c5dda2018c35d87be9b83de0890cb92ac0791fd59498847fc4eca6ff56671d", size = 566819, upload-time = "2025-08-21T04:21:23.31Z" }, - { url = "https://files.pythonhosted.org/packages/37/cd/86c4083e0f811f48f11bc0ddf1e7d13ef37adfd2fd4f78f2445f1cc5dec0/pyzmq-27.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:f54ca3e98f8f4d23e989c7d0edcf9da7a514ff261edaf64d1d8653dd5feb0a8b", size = 633264, upload-time = "2025-08-21T04:21:24.761Z" }, - { url = "https://files.pythonhosted.org/packages/a0/69/5b8bb6a19a36a569fac02153a9e083738785892636270f5f68a915956aea/pyzmq-27.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:2ef3067cb5b51b090fb853f423ad7ed63836ec154374282780a62eb866bf5768", size = 559316, upload-time = "2025-08-21T04:21:26.1Z" }, - { url = "https://files.pythonhosted.org/packages/68/69/b3a729e7b03e412bee2b1823ab8d22e20a92593634f664afd04c6c9d9ac0/pyzmq-27.0.2-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:5da05e3c22c95e23bfc4afeee6ff7d4be9ff2233ad6cb171a0e8257cd46b169a", size = 1305910, upload-time = "2025-08-21T04:21:27.609Z" }, - { url = "https://files.pythonhosted.org/packages/15/b7/f6a6a285193d489b223c340b38ee03a673467cb54914da21c3d7849f1b10/pyzmq-27.0.2-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e4520577971d01d47e2559bb3175fce1be9103b18621bf0b241abe0a933d040", size = 895507, upload-time = "2025-08-21T04:21:29.005Z" }, - { url = "https://files.pythonhosted.org/packages/17/e6/c4ed2da5ef9182cde1b1f5d0051a986e76339d71720ec1a00be0b49275ad/pyzmq-27.0.2-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d7de7bf73165b90bd25a8668659ccb134dd28449116bf3c7e9bab5cf8a8ec9", size = 652670, upload-time = "2025-08-21T04:21:30.71Z" }, - { url = "https://files.pythonhosted.org/packages/0e/66/d781ab0636570d32c745c4e389b1c6b713115905cca69ab6233508622edd/pyzmq-27.0.2-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340e7cddc32f147c6c00d116a3f284ab07ee63dbd26c52be13b590520434533c", size = 840581, upload-time = "2025-08-21T04:21:32.008Z" }, - { url = "https://files.pythonhosted.org/packages/a6/df/f24790caf565d72544f5c8d8500960b9562c1dc848d6f22f3c7e122e73d4/pyzmq-27.0.2-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba95693f9df8bb4a9826464fb0fe89033936f35fd4a8ff1edff09a473570afa0", size = 1641931, upload-time = "2025-08-21T04:21:33.371Z" }, - { url = "https://files.pythonhosted.org/packages/65/65/77d27b19fc5e845367f9100db90b9fce924f611b14770db480615944c9c9/pyzmq-27.0.2-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:ca42a6ce2d697537da34f77a1960d21476c6a4af3e539eddb2b114c3cf65a78c", size = 2021226, upload-time = "2025-08-21T04:21:35.301Z" }, - { url = "https://files.pythonhosted.org/packages/5b/65/1ed14421ba27a4207fa694772003a311d1142b7f543179e4d1099b7eb746/pyzmq-27.0.2-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3e44e665d78a07214b2772ccbd4b9bcc6d848d7895f1b2d7653f047b6318a4f6", size = 1878047, upload-time = "2025-08-21T04:21:36.749Z" }, - { url = "https://files.pythonhosted.org/packages/dd/dc/e578549b89b40dc78a387ec471c2a360766690c0a045cd8d1877d401012d/pyzmq-27.0.2-cp312-abi3-win32.whl", hash = "sha256:272d772d116615397d2be2b1417b3b8c8bc8671f93728c2f2c25002a4530e8f6", size = 558757, upload-time = "2025-08-21T04:21:38.2Z" }, - { url = "https://files.pythonhosted.org/packages/b5/89/06600980aefcc535c758414da969f37a5194ea4cdb73b745223f6af3acfb/pyzmq-27.0.2-cp312-abi3-win_amd64.whl", hash = "sha256:734be4f44efba0aa69bf5f015ed13eb69ff29bf0d17ea1e21588b095a3147b8e", size = 619281, upload-time = "2025-08-21T04:21:39.909Z" }, - { url = "https://files.pythonhosted.org/packages/30/84/df8a5c089552d17c9941d1aea4314b606edf1b1622361dae89aacedc6467/pyzmq-27.0.2-cp312-abi3-win_arm64.whl", hash = "sha256:41f0bd56d9279392810950feb2785a419c2920bbf007fdaaa7f4a07332ae492d", size = 552680, upload-time = "2025-08-21T04:21:41.571Z" }, - { url = "https://files.pythonhosted.org/packages/c7/60/027d0032a1e3b1aabcef0e309b9ff8a4099bdd5a60ab38b36a676ff2bd7b/pyzmq-27.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e297784aea724294fe95e442e39a4376c2f08aa4fae4161c669f047051e31b02", size = 836007, upload-time = "2025-08-21T04:23:00.447Z" }, - { url = "https://files.pythonhosted.org/packages/25/20/2ed1e6168aaea323df9bb2c451309291f53ba3af372ffc16edd4ce15b9e5/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3659a79ded9745bc9c2aef5b444ac8805606e7bc50d2d2eb16dc3ab5483d91f", size = 799932, upload-time = "2025-08-21T04:23:02.052Z" }, - { url = "https://files.pythonhosted.org/packages/fd/25/5c147307de546b502c9373688ce5b25dc22288d23a1ebebe5d587bf77610/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3dba49ff037d02373a9306b58d6c1e0be031438f822044e8767afccfdac4c6b", size = 567459, upload-time = "2025-08-21T04:23:03.593Z" }, - { url = "https://files.pythonhosted.org/packages/71/06/0dc56ffc615c8095cd089c9b98ce5c733e990f09ce4e8eea4aaf1041a532/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de84e1694f9507b29e7b263453a2255a73e3d099d258db0f14539bad258abe41", size = 747088, upload-time = "2025-08-21T04:23:05.334Z" }, - { url = "https://files.pythonhosted.org/packages/06/f6/4a50187e023b8848edd3f0a8e197b1a7fb08d261d8c60aae7cb6c3d71612/pyzmq-27.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f0944d65ba2b872b9fcece08411d6347f15a874c775b4c3baae7f278550da0fb", size = 544639, upload-time = "2025-08-21T04:23:07.279Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] @@ -4620,28 +4653,28 @@ wheels = [ [[package]] name = "ruamel-yaml-clib" -version = "0.2.12" +version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/fd/32c91c4d301ebe7931a3c44f7593650613afe14854e3cc9d2764321b7a46/ruamel.yaml.clib-0.2.13.tar.gz", hash = "sha256:8d4f8d7853053a5a19171a0f515f1e14f503bd3e772c4da6bafe7d0893d4f299", size = 201264, upload-time = "2025-09-22T13:33:47.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" }, - { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload-time = "2024-10-20T10:12:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload-time = "2024-10-21T11:26:41.438Z" }, - { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload-time = "2024-10-21T11:26:43.62Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload-time = "2024-12-11T19:58:15.592Z" }, - { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload-time = "2024-10-20T10:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload-time = "2024-10-20T10:12:54.652Z" }, - { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433, upload-time = "2024-10-20T10:12:55.657Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362, upload-time = "2024-10-20T10:12:57.155Z" }, - { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118, upload-time = "2024-10-20T10:12:58.501Z" }, - { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497, upload-time = "2024-10-20T10:13:00.211Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042, upload-time = "2024-10-21T11:26:46.038Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831, upload-time = "2024-10-21T11:26:47.487Z" }, - { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692, upload-time = "2024-12-11T19:58:17.252Z" }, - { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777, upload-time = "2024-10-20T10:13:01.395Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523, upload-time = "2024-10-20T10:13:02.768Z" }, + { url = "https://files.pythonhosted.org/packages/bb/86/fa6be53f11d1c663a261d137d616bbb326cb937b361a55a00cacf9ba76aa/ruamel.yaml.clib-0.2.13-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:825a7483da63b9448fdad9778269a5d02e5f64040aa8326fec071e830c2fc49c", size = 136894, upload-time = "2025-09-22T13:32:56.684Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/d26dbf3c53befd01d8b341cb29c555c1e910ef29478b4ebd50d1d67fbc64/ruamel.yaml.clib-0.2.13-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:54707e2200b9e1c34a62af09edd3ea3469a722951365311398458abe36ff45d6", size = 640033, upload-time = "2025-09-22T13:33:00.01Z" }, + { url = "https://files.pythonhosted.org/packages/a8/80/119ce9e40690b7e0dfc6bec41369333593f8738fa4f58dbbffdb9b80339b/ruamel.yaml.clib-0.2.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:958b1fc6334cd745db7099a69da4f60503cfbbc05ce1412b1d06c5922cd8314b", size = 738065, upload-time = "2025-09-22T13:32:57.712Z" }, + { url = "https://files.pythonhosted.org/packages/0b/85/09625df6b3ccf0ae0ddc93c0ab4d89f785debc0ae1e7efa541696b788b6a/ruamel.yaml.clib-0.2.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d50c415df5ba918c483a2486a90dd5a3e6df634fca688f71d266e15a618d643f", size = 700721, upload-time = "2025-09-22T13:32:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2e/c05050760b20fe10cb17b3fe7efd5d5653baefa862121f5b0825418f3d8b/ruamel.yaml.clib-0.2.13-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bbdb57e57a5bb3510bfb43496bab44857546c56c7f04bb7bfd0b248388d41c2e", size = 641589, upload-time = "2025-09-22T13:33:01.482Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e0/6cfd5c61070f0e5556a724c753919b8758b86fe89f5b53eb9f53d0506b27/ruamel.yaml.clib-0.2.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df8ef03eb60fc2e1c0570612363091898b97a7b3181422e1b34c3ee22d7e0a9e", size = 743806, upload-time = "2025-09-22T13:33:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/85/01/14964bd94bbe809497d1affc0625e428803cc9fd21e145b1b2edcbbc3c92/ruamel.yaml.clib-0.2.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89168de27694f7903a1317381525efdf7ea2c377b5b1eeeb927940870a7e8945", size = 769530, upload-time = "2025-09-22T13:33:04.044Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c0/a39cc7de38b5acc81241dda9888ecabc1ee90fb3ebf9189ddfc65c08555d/ruamel.yaml.clib-0.2.13-cp311-cp311-win32.whl", hash = "sha256:8cdb4c720137bb7834cf2c09753d5f3468023eebb7f6ad2604ddf920328753dd", size = 100254, upload-time = "2025-09-22T13:33:06.243Z" }, + { url = "https://files.pythonhosted.org/packages/1f/7f/d5c1e0279df8dd9ca92138bec8387a07112d97598f668ccb3190d0bc06aa/ruamel.yaml.clib-0.2.13-cp311-cp311-win_amd64.whl", hash = "sha256:670d8a9c5b22af152863236b7a7fec471aafefc5e89b637b8f98d280cb92a0ce", size = 118235, upload-time = "2025-09-22T13:33:05.202Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/b6b3ed185206af04c779eec77a4a57587278b713b72034957d127307bbef/ruamel.yaml.clib-0.2.13-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:810500056a0e294131042eca6687a786e82aad817e8e0bc5ce059d2f95b67fef", size = 137955, upload-time = "2025-09-22T13:33:07.585Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2b/b0307fc587cebabd8faaaeb1476c825fe4dbb5ad2b5c152dfbcc31b258c4/ruamel.yaml.clib-0.2.13-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f8e44b1b583f93a8f4d00e5285d9ea8ccd9524cdd4c54788db2880a11f21287d", size = 645916, upload-time = "2025-09-22T13:33:11.619Z" }, + { url = "https://files.pythonhosted.org/packages/74/a9/2ddad6eb03195206aca765b4ff7bd7098c46fbc65957d5ac53f96ef8d6f4/ruamel.yaml.clib-0.2.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1ab027be86f7d5ccc9b777a22194fd3850012df413f216bf1608768b4daa2f", size = 753115, upload-time = "2025-09-22T13:33:08.793Z" }, + { url = "https://files.pythonhosted.org/packages/42/a0/ded38d60fc34f69afef6f3e379f896a80d34532bb6b4828e95f7003be610/ruamel.yaml.clib-0.2.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55f01d82d7d96213f0963609876ae841c81e217985c668100c5fb95cc9a564b3", size = 703451, upload-time = "2025-09-22T13:33:10.111Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/121067d5621a77913dc0fd473bdf2ec4a515c564806b107d3b1257cd5b14/ruamel.yaml.clib-0.2.13-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:031c7cdd5751cda19ddf90e42756ec2ee72a30791c2f65f5f800f9ee929862f0", size = 647403, upload-time = "2025-09-22T13:33:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/15/fc/0845929db1840eec6aa6133b8c89941e251c3bc67180aee7d71a30bc1c80/ruamel.yaml.clib-0.2.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba745411caabc994963729663a7c2d09398795c36dc4b5672ca4fdc445ab6544", size = 745860, upload-time = "2025-09-22T13:33:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0a/dd0007d41a321edf64c4bda0aab70936281ee213406e6913105499f7d62a/ruamel.yaml.clib-0.2.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb90267cc3748858236b9cedb580191be1d0d218f6cde64e954fa759d90fb08d", size = 770202, upload-time = "2025-09-22T13:33:15.181Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/eeed0a9824b14fa089a82247fca96de6b202af84e4093ba2a15afc0b4cb9/ruamel.yaml.clib-0.2.13-cp312-cp312-win32.whl", hash = "sha256:5188c3212c10fcd14a6de8ce787c0713a49fc5972054703f790cfd9f1d27a90b", size = 98831, upload-time = "2025-09-22T13:33:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/25/45/c882a32e4b5c0ed6a5b4e0933af427fef48f3aad7259b87f38e33ee20536/ruamel.yaml.clib-0.2.13-cp312-cp312-win_amd64.whl", hash = "sha256:518b13f9fe601a559a759f3d21cdb2590cb85611934a7c0f09c09dc1a869ec83", size = 115567, upload-time = "2025-09-22T13:33:16.358Z" }, ] [[package]] @@ -4655,28 +4688,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.11" +version = "0.13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987, upload-time = "2025-09-18T19:52:44.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" }, - { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" }, - { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" }, - { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" }, - { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" }, - { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" }, + { url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308, upload-time = "2025-09-18T19:51:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258, upload-time = "2025-09-18T19:52:00.184Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554, upload-time = "2025-09-18T19:52:02.753Z" }, + { url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181, upload-time = "2025-09-18T19:52:05.279Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599, upload-time = "2025-09-18T19:52:07.497Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178, upload-time = "2025-09-18T19:52:10.189Z" }, + { url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474, upload-time = "2025-09-18T19:52:12.866Z" }, + { url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531, upload-time = "2025-09-18T19:52:15.245Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267, upload-time = "2025-09-18T19:52:17.649Z" }, + { url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120, upload-time = "2025-09-18T19:52:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084, upload-time = "2025-09-18T19:52:23.032Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105, upload-time = "2025-09-18T19:52:25.263Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284, upload-time = "2025-09-18T19:52:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314, upload-time = "2025-09-18T19:52:30.212Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360, upload-time = "2025-09-18T19:52:32.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448, upload-time = "2025-09-18T19:52:35.545Z" }, + { url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458, upload-time = "2025-09-18T19:52:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893, upload-time = "2025-09-18T19:52:41.283Z" }, ] [[package]] @@ -4690,47 +4723,46 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.35.2" +version = "2.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/79/0ecb942f3f1ad26c40c27f81ff82392d85c01d26a45e3c72c2b37807e680/sentry_sdk-2.35.2.tar.gz", hash = "sha256:e9e8f3c795044beb59f2c8f4c6b9b0f9779e5e604099882df05eec525e782cc6", size = 343377, upload-time = "2025-09-01T11:00:58.633Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/22/60fd703b34d94d216b2387e048ac82de3e86b63bc28869fb076f8bb0204a/sentry_sdk-2.38.0.tar.gz", hash = "sha256:792d2af45e167e2f8a3347143f525b9b6bac6f058fb2014720b40b84ccbeb985", size = 348116, upload-time = "2025-09-15T15:00:37.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/91/a43308dc82a0e32d80cd0dfdcfca401ecbd0f431ab45f24e48bb97b7800d/sentry_sdk-2.35.2-py2.py3-none-any.whl", hash = "sha256:38c98e3cbb620dd3dd80a8d6e39c753d453dd41f8a9df581b0584c19a52bc926", size = 363975, upload-time = "2025-09-01T11:00:56.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/bde4c4bbb269b71bc09316af8eb00da91f67814d40337cc12ef9c8742541/sentry_sdk-2.38.0-py2.py3-none-any.whl", hash = "sha256:2324aea8573a3fa1576df7fb4d65c4eb8d9929c8fa5939647397a07179eef8d0", size = 370346, upload-time = "2025-09-15T15:00:35.821Z" }, ] [[package]] name = "setproctitle" -version = "1.3.6" +version = "1.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/af/56efe21c53ac81ac87e000b15e60b3d8104224b4313b6eacac3597bd183d/setproctitle-1.3.6.tar.gz", hash = "sha256:c9f32b96c700bb384f33f7cf07954bb609d35dd82752cef57fb2ee0968409169", size = 26889, upload-time = "2025-04-29T13:35:00.184Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/3b/8288d0cd969a63500dd62fc2c99ce6980f9909ccef0770ab1f86c361e0bf/setproctitle-1.3.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a1d856b0f4e4a33e31cdab5f50d0a14998f3a2d726a3fd5cb7c4d45a57b28d1b", size = 17412, upload-time = "2025-04-29T13:32:58.135Z" }, - { url = "https://files.pythonhosted.org/packages/39/37/43a5a3e25ca1048dbbf4db0d88d346226f5f1acd131bb8e660f4bfe2799f/setproctitle-1.3.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50706b9c0eda55f7de18695bfeead5f28b58aa42fd5219b3b1692d554ecbc9ec", size = 11963, upload-time = "2025-04-29T13:32:59.17Z" }, - { url = "https://files.pythonhosted.org/packages/5b/47/f103c40e133154783c91a10ab08ac9fc410ed835aa85bcf7107cb882f505/setproctitle-1.3.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af188f3305f0a65c3217c30c6d4c06891e79144076a91e8b454f14256acc7279", size = 31718, upload-time = "2025-04-29T13:33:00.36Z" }, - { url = "https://files.pythonhosted.org/packages/1f/13/7325dd1c008dd6c0ebd370ddb7505977054a87e406f142318e395031a792/setproctitle-1.3.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce0ed8b3f64c71c140f0ec244e5fdf8ecf78ddf8d2e591d4a8b6aa1c1214235", size = 33027, upload-time = "2025-04-29T13:33:01.499Z" }, - { url = "https://files.pythonhosted.org/packages/0c/0a/6075bfea05a71379d77af98a9ac61163e8b6e5ef1ae58cd2b05871b2079c/setproctitle-1.3.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70100e2087fe05359f249a0b5f393127b3a1819bf34dec3a3e0d4941138650c9", size = 30223, upload-time = "2025-04-29T13:33:03.259Z" }, - { url = "https://files.pythonhosted.org/packages/cc/41/fbf57ec52f4f0776193bd94334a841f0bc9d17e745f89c7790f336420c65/setproctitle-1.3.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1065ed36bd03a3fd4186d6c6de5f19846650b015789f72e2dea2d77be99bdca1", size = 31204, upload-time = "2025-04-29T13:33:04.455Z" }, - { url = "https://files.pythonhosted.org/packages/97/b5/f799fb7a00de29fb0ac1dfd015528dea425b9e31a8f1068a0b3df52d317f/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4adf6a0013fe4e0844e3ba7583ec203ca518b9394c6cc0d3354df2bf31d1c034", size = 31181, upload-time = "2025-04-29T13:33:05.697Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b7/81f101b612014ec61723436022c31146178813d6ca6b947f7b9c84e9daf4/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb7452849f6615871eabed6560ffedfe56bc8af31a823b6be4ce1e6ff0ab72c5", size = 30101, upload-time = "2025-04-29T13:33:07.223Z" }, - { url = "https://files.pythonhosted.org/packages/67/23/681232eed7640eab96719daa8647cc99b639e3daff5c287bd270ef179a73/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a094b7ce455ca341b59a0f6ce6be2e11411ba6e2860b9aa3dbb37468f23338f4", size = 32438, upload-time = "2025-04-29T13:33:08.538Z" }, - { url = "https://files.pythonhosted.org/packages/19/f8/4d075a7bdc3609ac71535b849775812455e4c40aedfbf0778a6f123b1774/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ad1c2c2baaba62823a7f348f469a967ece0062140ca39e7a48e4bbb1f20d54c4", size = 30625, upload-time = "2025-04-29T13:33:09.707Z" }, - { url = "https://files.pythonhosted.org/packages/5f/73/a2a8259ebee166aee1ca53eead75de0e190b3ddca4f716e5c7470ebb7ef6/setproctitle-1.3.6-cp311-cp311-win32.whl", hash = "sha256:8050c01331135f77ec99d99307bfbc6519ea24d2f92964b06f3222a804a3ff1f", size = 11488, upload-time = "2025-04-29T13:33:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/c9/15/52cf5e1ff0727d53704cfdde2858eaf237ce523b0b04db65faa84ff83e13/setproctitle-1.3.6-cp311-cp311-win_amd64.whl", hash = "sha256:9b73cf0fe28009a04a35bb2522e4c5b5176cc148919431dcb73fdbdfaab15781", size = 12201, upload-time = "2025-04-29T13:33:12.389Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fb/99456fd94d4207c5f6c40746a048a33a52b4239cd7d9c8d4889e2210ec82/setproctitle-1.3.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af44bb7a1af163806bbb679eb8432fa7b4fb6d83a5d403b541b675dcd3798638", size = 17399, upload-time = "2025-04-29T13:33:13.406Z" }, - { url = "https://files.pythonhosted.org/packages/d5/48/9699191fe6062827683c43bfa9caac33a2c89f8781dd8c7253fa3dba85fd/setproctitle-1.3.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cca16fd055316a48f0debfcbfb6af7cea715429fc31515ab3fcac05abd527d8", size = 11966, upload-time = "2025-04-29T13:33:14.976Z" }, - { url = "https://files.pythonhosted.org/packages/33/03/b085d192b9ecb9c7ce6ad6ef30ecf4110b7f39430b58a56245569827fcf4/setproctitle-1.3.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea002088d5554fd75e619742cefc78b84a212ba21632e59931b3501f0cfc8f67", size = 32017, upload-time = "2025-04-29T13:33:16.163Z" }, - { url = "https://files.pythonhosted.org/packages/ae/68/c53162e645816f97212002111420d1b2f75bf6d02632e37e961dc2cd6d8b/setproctitle-1.3.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb465dd5825356c1191a038a86ee1b8166e3562d6e8add95eec04ab484cfb8a2", size = 33419, upload-time = "2025-04-29T13:33:18.239Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0d/119a45d15a816a6cf5ccc61b19729f82620095b27a47e0a6838216a95fae/setproctitle-1.3.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2c8e20487b3b73c1fa72c56f5c89430617296cd380373e7af3a538a82d4cd6d", size = 30711, upload-time = "2025-04-29T13:33:19.571Z" }, - { url = "https://files.pythonhosted.org/packages/e3/fb/5e9b5068df9e9f31a722a775a5e8322a29a638eaaa3eac5ea7f0b35e6314/setproctitle-1.3.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d6252098e98129a1decb59b46920d4eca17b0395f3d71b0d327d086fefe77d", size = 31742, upload-time = "2025-04-29T13:33:21.172Z" }, - { url = "https://files.pythonhosted.org/packages/35/88/54de1e73e8fce87d587889c7eedb48fc4ee2bbe4e4ca6331690d03024f86/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf355fbf0d4275d86f9f57be705d8e5eaa7f8ddb12b24ced2ea6cbd68fdb14dc", size = 31925, upload-time = "2025-04-29T13:33:22.427Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/65948d7badd66e63e3db247b923143da142790fa293830fdecf832712c2d/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e288f8a162d663916060beb5e8165a8551312b08efee9cf68302687471a6545d", size = 30981, upload-time = "2025-04-29T13:33:23.739Z" }, - { url = "https://files.pythonhosted.org/packages/22/20/c495e61786f1d38d5dc340b9d9077fee9be3dfc7e89f515afe12e1526dbc/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b2e54f4a2dc6edf0f5ea5b1d0a608d2af3dcb5aa8c8eeab9c8841b23e1b054fe", size = 33209, upload-time = "2025-04-29T13:33:24.915Z" }, - { url = "https://files.pythonhosted.org/packages/98/3f/a457b8550fbd34d5b482fe20b8376b529e76bf1fbf9a474a6d9a641ab4ad/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b6f4abde9a2946f57e8daaf1160b2351bcf64274ef539e6675c1d945dbd75e2a", size = 31587, upload-time = "2025-04-29T13:33:26.123Z" }, - { url = "https://files.pythonhosted.org/packages/44/fe/743517340e5a635e3f1c4310baea20c16c66202f96a6f4cead222ffd6d84/setproctitle-1.3.6-cp312-cp312-win32.whl", hash = "sha256:db608db98ccc21248370d30044a60843b3f0f3d34781ceeea67067c508cd5a28", size = 11487, upload-time = "2025-04-29T13:33:27.403Z" }, - { url = "https://files.pythonhosted.org/packages/60/9a/d88f1c1f0f4efff1bd29d9233583ee341114dda7d9613941453984849674/setproctitle-1.3.6-cp312-cp312-win_amd64.whl", hash = "sha256:082413db8a96b1f021088e8ec23f0a61fec352e649aba20881895815388b66d3", size = 12208, upload-time = "2025-04-29T13:33:28.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, ] [[package]] @@ -4771,22 +4803,22 @@ wheels = [ [[package]] name = "siphash24" -version = "1.7" +version = "1.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/be/f0a0ffbb00c51c5633b41459b5ce9b017c025a9256b4403e648c18e70850/siphash24-1.7.tar.gz", hash = "sha256:6e90fee5f199ea25b4e7303646b31872a437174fe885a93dbd4cf7784eb48164", size = 19801, upload-time = "2024-10-15T13:41:51.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/a2/e049b6fccf7a94bd1b2f68b3059a7d6a7aea86a808cac80cb9ae71ab6254/siphash24-1.8.tar.gz", hash = "sha256:aa932f0af4a7335caef772fdaf73a433a32580405c41eb17ff24077944b0aa97", size = 19946, upload-time = "2025-09-02T20:42:04.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/67/4ffd23a848739966e1b314ef99f6410035bccee00be14261313787b8f506/siphash24-1.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de75488e93f1cd12c8d5004efd1ebd958c0265205a9d73e8dd8b071900838841", size = 80493, upload-time = "2024-10-15T13:41:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/56/bd/ec198a8c7aef65e967ae84f633bd9950d784c9e527d738c9a3e4bccc34a5/siphash24-1.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffca9908450f9f346e97a223185fcd16217d67d84c6f246f3080c4224f41a514", size = 75350, upload-time = "2024-10-15T13:41:16.262Z" }, - { url = "https://files.pythonhosted.org/packages/50/5a/77838c916bd15addfc2e51286db4c442cb12e25eb4f8d296c394c2280240/siphash24-1.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8ff44ce166452993fea267ea1b2fd089d8e7f103b13d360da441f12b0df121d", size = 100567, upload-time = "2024-10-15T13:41:17.435Z" }, - { url = "https://files.pythonhosted.org/packages/f0/aa/736a0a2efae9a6f69ac1ee4d28c2274fcad2150349fac752d6c525c4e06e/siphash24-1.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4062548dcb1eef13bbe0356d6f8675bfe4571ef38d7103445daa82ba167240d1", size = 105630, upload-time = "2024-10-15T13:41:18.578Z" }, - { url = "https://files.pythonhosted.org/packages/79/52/1afbd70142d3db093d49197e3abe15ca2f1a14678299327ba776944b4771/siphash24-1.7-cp311-cp311-win32.whl", hash = "sha256:7b4ea29376b688fbcc3d25707c15a9dfe7b4ebbc4322878d75bb77e199210a39", size = 67648, upload-time = "2024-10-15T13:41:19.606Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1d/bedcd04c2d1d199c9f6b3e61a6caae0e17257696c9f49594e49856b17a99/siphash24-1.7-cp311-cp311-win_amd64.whl", hash = "sha256:ec06104e6ef1e512ee30f1b8aeae2b83c0f55f12a94042f0df5a87d43a1f4c52", size = 80046, upload-time = "2024-10-15T13:41:20.654Z" }, - { url = "https://files.pythonhosted.org/packages/3e/62/93e552af9535a416f684327f870143ee42fc9e816091672467cdfd62cce6/siphash24-1.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:76a64ff0cdd192e4d0a391956d9b121c56ed56e773c5ab7eb7c3e035fd16e8cb", size = 82084, upload-time = "2024-10-15T13:41:21.776Z" }, - { url = "https://files.pythonhosted.org/packages/59/3e/b0791ab53aa9ac191b71a021eab2e75baa7c27d7feb7ec148d7961d148ba/siphash24-1.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ca649bc7437d614f758891deade3b187832792a853269219e77f10509f82fe", size = 76233, upload-time = "2024-10-15T13:41:22.787Z" }, - { url = "https://files.pythonhosted.org/packages/29/4c/4c1b809bf302e9b60f3ec09ba115b2a4ac1ff6755735ee8884924fcdb45e/siphash24-1.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc37dd0aed23f76bd257fbd2953fd5d954b329d7463c6ff57263a2699c52dde6", size = 98188, upload-time = "2024-10-15T13:41:24.327Z" }, - { url = "https://files.pythonhosted.org/packages/96/bf/e6b49f8ff88130bd224f291ea77d30fdde4df5f6572c519aca5d8fc8a27c/siphash24-1.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea490a200891905856b6ad0f9c56d4ec787876220bcb34c49441b2566b97887", size = 102946, upload-time = "2024-10-15T13:41:25.633Z" }, - { url = "https://files.pythonhosted.org/packages/3d/75/45c831626013950fb2ea715c218c3397e5cf2328a67208bf5d8ff69aa9e6/siphash24-1.7-cp312-cp312-win32.whl", hash = "sha256:69eb8c2c112a738875bb283cd53ef5e86874bc5aed17f3020b38e9174208fb79", size = 68323, upload-time = "2024-10-15T13:41:27.349Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d3/39190c40a68defd19b99c1082dd7455543a52283803bfa111b0e45fae968/siphash24-1.7-cp312-cp312-win_amd64.whl", hash = "sha256:7459569ea4669b6feeaf7d299fc5157cc5c69ca1231dc0decb7a7da2397c782e", size = 81000, upload-time = "2024-10-15T13:41:28.364Z" }, + { url = "https://files.pythonhosted.org/packages/82/23/f53f5bd8866c6ea3abe434c9f208e76ea027210d8b75cd0e0dc849661c7a/siphash24-1.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4662ac616bce4d3c9d6003a0d398e56f8be408fc53a166b79fad08d4f34268e", size = 76930, upload-time = "2025-09-02T20:41:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/0b/25/aebf246904424a06e7ffb7a40cfa9ea9e590ea0fac82e182e0f5d1f1d7ef/siphash24-1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:53d6bed0951a99c6d2891fa6f8acfd5ca80c3e96c60bcee99f6fa01a04773b1c", size = 74315, upload-time = "2025-09-02T20:41:02.38Z" }, + { url = "https://files.pythonhosted.org/packages/59/3f/7010407c3416ef052d46550d54afb2581fb247018fc6500af8c66669eff2/siphash24-1.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d114c03648630e9e07dac2fe95442404e4607adca91640d274ece1a4fa71123e", size = 99756, upload-time = "2025-09-02T20:41:03.902Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/09c734833e69badd7e3faed806b4372bd6564ae0946bd250d5239885914f/siphash24-1.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88c1a55ff82b127c5d3b96927a430d8859e6a98846a5b979833ac790682dd91b", size = 104044, upload-time = "2025-09-02T20:41:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/24/30/56a26d9141a34433da221f732599e2b23d2d70a966c249a9f00feb9a2915/siphash24-1.8-cp311-cp311-win32.whl", hash = "sha256:9430255e6a1313470f52c07c4a4643c451a5b2853f6d4008e4dda05cafb6ce7c", size = 62196, upload-time = "2025-09-02T20:41:07.299Z" }, + { url = "https://files.pythonhosted.org/packages/47/b2/11b0ae63fd374652544e1b12f72ba2cc3fe6c93c1483bd8ff6935b0a8a4b/siphash24-1.8-cp311-cp311-win_amd64.whl", hash = "sha256:1e4b37e4ef0b4496169adce2a58b6c3f230b5852dfa5f7ad0b2d664596409e47", size = 77162, upload-time = "2025-09-02T20:41:08.878Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/ce3545ce8052ac7ca104b183415a27ec3335e5ed51978fdd7b433f3cfe5b/siphash24-1.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5ed437c6e6cc96196b38728e57cd30b0427df45223475a90e173f5015ef5ba", size = 78136, upload-time = "2025-09-02T20:41:10.083Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/896c3b91bc9deb78c415448b1db67343917f35971a9e23a5967a9d323b8a/siphash24-1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4ef78abdf811325c7089a35504df339c48c0007d4af428a044431d329721e56", size = 74588, upload-time = "2025-09-02T20:41:11.251Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/8dad3f5601db485ba862e1c1f91a5d77fb563650856a6708e9acb40ee53c/siphash24-1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:065eff55c4fefb3a29fd26afb2c072abf7f668ffd53b91d41f92a1c485fcbe5c", size = 98655, upload-time = "2025-09-02T20:41:12.45Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/e0c352624c1f2faad270aeb5cce6e173977ef66b9b5e918aa6f32af896bf/siphash24-1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6fa84ebfd47677262aa0bcb0f5a70f796f5fc5704b287ee1b65a3bd4fb7a5d", size = 103217, upload-time = "2025-09-02T20:41:13.746Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f6/0b1675bea4d40affcae642d9c7337702a4138b93c544230280712403e968/siphash24-1.8-cp312-cp312-win32.whl", hash = "sha256:6582f73615552ca055e51e03cb02a28e570a641a7f500222c86c2d811b5037eb", size = 63114, upload-time = "2025-09-02T20:41:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/afefef85d72ed8b5cf1aa9283f712e3cd43c9682fabbc809dec54baa8452/siphash24-1.8-cp312-cp312-win_amd64.whl", hash = "sha256:44ea6d794a7cbe184e1e1da2df81c5ebb672ab3867935c3e87c08bb0c2fa4879", size = 76232, upload-time = "2025-09-02T20:41:16.112Z" }, ] [[package]] @@ -4833,9 +4865,9 @@ wheels = [ [[package]] name = "spidev" -version = "3.7" +version = "3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/99/dd50af8200e224ce9412ad01cdbeeb5b39b2d61acd72138f2b92c4a6d619/spidev-3.7.tar.gz", hash = "sha256:ce628a5ff489f45132679879bff5f455a66abf9751af01843850155b06ae92f0", size = 11616, upload-time = "2025-05-06T14:23:30.783Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/87/039b6eeea781598015b538691bc174cc0bf77df9d4d2d3b8bf9245c0de8c/spidev-3.8.tar.gz", hash = "sha256:2bc02fb8c6312d519ebf1f4331067427c0921d3f77b8bcaf05189a2e8b8382c0", size = 13893, upload-time = "2025-09-15T18:56:20.672Z" } [[package]] name = "sympy" @@ -4872,14 +4904,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.4.20250809" +version = "2.32.4.20250913" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, ] [[package]] @@ -5033,42 +5065,42 @@ wheels = [ [[package]] name = "zstandard" -version = "0.24.0" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/1b/c20b2ef1d987627765dcd5bf1dadb8ef6564f00a87972635099bb76b7a05/zstandard-0.24.0.tar.gz", hash = "sha256:fe3198b81c00032326342d973e526803f183f97aa9e9a98e3f897ebafe21178f", size = 905681, upload-time = "2025-08-17T18:36:36.352Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/1f/5c72806f76043c0ef9191a2b65281dacdf3b65b0828eb13bb2c987c4fb90/zstandard-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:addfc23e3bd5f4b6787b9ca95b2d09a1a67ad5a3c318daaa783ff90b2d3a366e", size = 795228, upload-time = "2025-08-17T18:21:46.978Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ba/3059bd5cd834666a789251d14417621b5c61233bd46e7d9023ea8bc1043a/zstandard-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b005bcee4be9c3984b355336283afe77b2defa76ed6b89332eced7b6fa68b68", size = 640520, upload-time = "2025-08-17T18:21:48.162Z" }, - { url = "https://files.pythonhosted.org/packages/57/07/f0e632bf783f915c1fdd0bf68614c4764cae9dd46ba32cbae4dd659592c3/zstandard-0.24.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:3f96a9130171e01dbb6c3d4d9925d604e2131a97f540e223b88ba45daf56d6fb", size = 5347682, upload-time = "2025-08-17T18:21:50.266Z" }, - { url = "https://files.pythonhosted.org/packages/a6/4c/63523169fe84773a7462cd090b0989cb7c7a7f2a8b0a5fbf00009ba7d74d/zstandard-0.24.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd0d3d16e63873253bad22b413ec679cf6586e51b5772eb10733899832efec42", size = 5057650, upload-time = "2025-08-17T18:21:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/c6/16/49013f7ef80293f5cebf4c4229535a9f4c9416bbfd238560edc579815dbe/zstandard-0.24.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b7a8c30d9bf4bd5e4dcfe26900bef0fcd9749acde45cdf0b3c89e2052fda9a13", size = 5404893, upload-time = "2025-08-17T18:21:54.54Z" }, - { url = "https://files.pythonhosted.org/packages/4d/38/78e8bcb5fc32a63b055f2b99e0be49b506f2351d0180173674f516cf8a7a/zstandard-0.24.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:52cd7d9fa0a115c9446abb79b06a47171b7d916c35c10e0c3aa6f01d57561382", size = 5452389, upload-time = "2025-08-17T18:21:56.822Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/81671f05619edbacd49bd84ce6899a09fc8299be20c09ae92f6618ccb92d/zstandard-0.24.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0f6fc2ea6e07e20df48752e7700e02e1892c61f9a6bfbacaf2c5b24d5ad504b", size = 5558888, upload-time = "2025-08-17T18:21:58.68Z" }, - { url = "https://files.pythonhosted.org/packages/49/cc/e83feb2d7d22d1f88434defbaeb6e5e91f42a4f607b5d4d2d58912b69d67/zstandard-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e46eb6702691b24ddb3e31e88b4a499e31506991db3d3724a85bd1c5fc3cfe4e", size = 5048038, upload-time = "2025-08-17T18:22:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/08/c3/7a5c57ff49ef8943877f85c23368c104c2aea510abb339a2dc31ad0a27c3/zstandard-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5e3b9310fd7f0d12edc75532cd9a56da6293840c84da90070d692e0bb15f186", size = 5573833, upload-time = "2025-08-17T18:22:02.402Z" }, - { url = "https://files.pythonhosted.org/packages/f9/00/64519983cd92535ba4bdd4ac26ac52db00040a52d6c4efb8d1764abcc343/zstandard-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76cdfe7f920738ea871f035568f82bad3328cbc8d98f1f6988264096b5264efd", size = 4961072, upload-time = "2025-08-17T18:22:04.384Z" }, - { url = "https://files.pythonhosted.org/packages/72/ab/3a08a43067387d22994fc87c3113636aa34ccd2914a4d2d188ce365c5d85/zstandard-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3f2fe35ec84908dddf0fbf66b35d7c2878dbe349552dd52e005c755d3493d61c", size = 5268462, upload-time = "2025-08-17T18:22:06.095Z" }, - { url = "https://files.pythonhosted.org/packages/49/cf/2abb3a1ad85aebe18c53e7eca73223f1546ddfa3bf4d2fb83fc5a064c5ca/zstandard-0.24.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:aa705beb74ab116563f4ce784fa94771f230c05d09ab5de9c397793e725bb1db", size = 5443319, upload-time = "2025-08-17T18:22:08.572Z" }, - { url = "https://files.pythonhosted.org/packages/40/42/0dd59fc2f68f1664cda11c3b26abdf987f4e57cb6b6b0f329520cd074552/zstandard-0.24.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:aadf32c389bb7f02b8ec5c243c38302b92c006da565e120dfcb7bf0378f4f848", size = 5822355, upload-time = "2025-08-17T18:22:10.537Z" }, - { url = "https://files.pythonhosted.org/packages/99/c0/ea4e640fd4f7d58d6f87a1e7aca11fb886ac24db277fbbb879336c912f63/zstandard-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e40cd0fc734aa1d4bd0e7ad102fd2a1aefa50ce9ef570005ffc2273c5442ddc3", size = 5365257, upload-time = "2025-08-17T18:22:13.159Z" }, - { url = "https://files.pythonhosted.org/packages/27/a9/92da42a5c4e7e4003271f2e1f0efd1f37cfd565d763ad3604e9597980a1c/zstandard-0.24.0-cp311-cp311-win32.whl", hash = "sha256:cda61c46343809ecda43dc620d1333dd7433a25d0a252f2dcc7667f6331c7b61", size = 435559, upload-time = "2025-08-17T18:22:17.29Z" }, - { url = "https://files.pythonhosted.org/packages/e2/8e/2c8e5c681ae4937c007938f954a060fa7c74f36273b289cabdb5ef0e9a7e/zstandard-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:3b95fc06489aa9388400d1aab01a83652bc040c9c087bd732eb214909d7fb0dd", size = 505070, upload-time = "2025-08-17T18:22:14.808Z" }, - { url = "https://files.pythonhosted.org/packages/52/10/a2f27a66bec75e236b575c9f7b0d7d37004a03aa2dcde8e2decbe9ed7b4d/zstandard-0.24.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad9fd176ff6800a0cf52bcf59c71e5de4fa25bf3ba62b58800e0f84885344d34", size = 461507, upload-time = "2025-08-17T18:22:15.964Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/0bd281d9154bba7fc421a291e263911e1d69d6951aa80955b992a48289f6/zstandard-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2bda8f2790add22773ee7a4e43c90ea05598bffc94c21c40ae0a9000b0133c3", size = 795710, upload-time = "2025-08-17T18:22:19.189Z" }, - { url = "https://files.pythonhosted.org/packages/36/26/b250a2eef515caf492e2d86732e75240cdac9d92b04383722b9753590c36/zstandard-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc76de75300f65b8eb574d855c12518dc25a075dadb41dd18f6322bda3fe15d5", size = 640336, upload-time = "2025-08-17T18:22:20.466Z" }, - { url = "https://files.pythonhosted.org/packages/79/bf/3ba6b522306d9bf097aac8547556b98a4f753dc807a170becaf30dcd6f01/zstandard-0.24.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d2b3b4bda1a025b10fe0269369475f420177f2cb06e0f9d32c95b4873c9f80b8", size = 5342533, upload-time = "2025-08-17T18:22:22.326Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ec/22bc75bf054e25accdf8e928bc68ab36b4466809729c554ff3a1c1c8bce6/zstandard-0.24.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b84c6c210684286e504022d11ec294d2b7922d66c823e87575d8b23eba7c81f", size = 5062837, upload-time = "2025-08-17T18:22:24.416Z" }, - { url = "https://files.pythonhosted.org/packages/48/cc/33edfc9d286e517fb5b51d9c3210e5bcfce578d02a675f994308ca587ae1/zstandard-0.24.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c59740682a686bf835a1a4d8d0ed1eefe31ac07f1c5a7ed5f2e72cf577692b00", size = 5393855, upload-time = "2025-08-17T18:22:26.786Z" }, - { url = "https://files.pythonhosted.org/packages/73/36/59254e9b29da6215fb3a717812bf87192d89f190f23817d88cb8868c47ac/zstandard-0.24.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6324fde5cf5120fbf6541d5ff3c86011ec056e8d0f915d8e7822926a5377193a", size = 5451058, upload-time = "2025-08-17T18:22:28.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/c7/31674cb2168b741bbbe71ce37dd397c9c671e73349d88ad3bca9e9fae25b/zstandard-0.24.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51a86bd963de3f36688553926a84e550d45d7f9745bd1947d79472eca27fcc75", size = 5546619, upload-time = "2025-08-17T18:22:31.115Z" }, - { url = "https://files.pythonhosted.org/packages/e6/01/1a9f22239f08c00c156f2266db857545ece66a6fc0303d45c298564bc20b/zstandard-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d82ac87017b734f2fb70ff93818c66f0ad2c3810f61040f077ed38d924e19980", size = 5046676, upload-time = "2025-08-17T18:22:33.077Z" }, - { url = "https://files.pythonhosted.org/packages/a7/91/6c0cf8fa143a4988a0361380ac2ef0d7cb98a374704b389fbc38b5891712/zstandard-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92ea7855d5bcfb386c34557516c73753435fb2d4a014e2c9343b5f5ba148b5d8", size = 5576381, upload-time = "2025-08-17T18:22:35.391Z" }, - { url = "https://files.pythonhosted.org/packages/e2/77/1526080e22e78871e786ccf3c84bf5cec9ed25110a9585507d3c551da3d6/zstandard-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3adb4b5414febf074800d264ddf69ecade8c658837a83a19e8ab820e924c9933", size = 4953403, upload-time = "2025-08-17T18:22:37.266Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d0/a3a833930bff01eab697eb8abeafb0ab068438771fa066558d96d7dafbf9/zstandard-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6374feaf347e6b83ec13cc5dcfa70076f06d8f7ecd46cc71d58fac798ff08b76", size = 5267396, upload-time = "2025-08-17T18:22:39.757Z" }, - { url = "https://files.pythonhosted.org/packages/f3/5e/90a0db9a61cd4769c06374297ecfcbbf66654f74cec89392519deba64d76/zstandard-0.24.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:13fc548e214df08d896ee5f29e1f91ee35db14f733fef8eabea8dca6e451d1e2", size = 5433269, upload-time = "2025-08-17T18:22:42.131Z" }, - { url = "https://files.pythonhosted.org/packages/ce/58/fc6a71060dd67c26a9c5566e0d7c99248cbe5abfda6b3b65b8f1a28d59f7/zstandard-0.24.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0a416814608610abf5488889c74e43ffa0343ca6cf43957c6b6ec526212422da", size = 5814203, upload-time = "2025-08-17T18:22:44.017Z" }, - { url = "https://files.pythonhosted.org/packages/5c/6a/89573d4393e3ecbfa425d9a4e391027f58d7810dec5cdb13a26e4cdeef5c/zstandard-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0d66da2649bb0af4471699aeb7a83d6f59ae30236fb9f6b5d20fb618ef6c6777", size = 5359622, upload-time = "2025-08-17T18:22:45.802Z" }, - { url = "https://files.pythonhosted.org/packages/60/ff/2cbab815d6f02a53a9d8d8703bc727d8408a2e508143ca9af6c3cca2054b/zstandard-0.24.0-cp312-cp312-win32.whl", hash = "sha256:ff19efaa33e7f136fe95f9bbcc90ab7fb60648453b03f95d1de3ab6997de0f32", size = 435968, upload-time = "2025-08-17T18:22:49.493Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/8f96b8ddb7ad12344218fbd0fd2805702dafd126ae9f8a1fb91eef7b33da/zstandard-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc05f8a875eb651d1cc62e12a4a0e6afa5cd0cc231381adb830d2e9c196ea895", size = 505195, upload-time = "2025-08-17T18:22:47.193Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4a/bfca20679da63bfc236634ef2e4b1b4254203098b0170e3511fee781351f/zstandard-0.24.0-cp312-cp312-win_arm64.whl", hash = "sha256:b04c94718f7a8ed7cdd01b162b6caa1954b3c9d486f00ecbbd300f149d2b2606", size = 461605, upload-time = "2025-08-17T18:22:48.317Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, ]