mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-09 22:24:46 +08:00
Compare commits
26 Commits
accel-pers
...
navigation
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82540646d7 | ||
|
|
f3d8b24bf4 | ||
|
|
880ed98ffc | ||
|
|
dcaf84d04c | ||
|
|
3a82a0797a | ||
|
|
2d1f3833e4 | ||
|
|
e28dd1e1aa | ||
|
|
1a62ae821e | ||
|
|
1063114408 | ||
|
|
cefb344183 | ||
|
|
81b37712f1 | ||
|
|
1a4c48249b | ||
|
|
3d8763b3ce | ||
|
|
b2427a5f20 | ||
|
|
cf2b033c79 | ||
|
|
589e33f665 | ||
|
|
399ed08926 | ||
|
|
6aac50ab56 | ||
|
|
211c8adcce | ||
|
|
07b8e7783d | ||
|
|
53bf5b0d41 | ||
|
|
8c33592628 | ||
|
|
3bbb33f6bd | ||
|
|
5bd9549bd1 | ||
|
|
3481702715 | ||
|
|
c9781ee31d |
2
.github/workflows/ci_weekly_report.yaml
vendored
2
.github/workflows/ci_weekly_report.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
report:
|
||||
needs: [ci_matrix_run]
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && github.repository == 'commaai/openpilot'
|
||||
if: always()
|
||||
steps:
|
||||
- name: Get job results
|
||||
uses: actions/github-script@v7
|
||||
|
||||
151
.github/workflows/mici_raylib_ui_preview.yaml
vendored
151
.github/workflows/mici_raylib_ui_preview.yaml
vendored
@@ -1,151 +0,0 @@
|
||||
name: "mici raylib ui preview"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
types: [assigned, opened, synchronize, reopened, edited]
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- 'selfdrive/assets/**'
|
||||
- 'selfdrive/ui/**'
|
||||
- 'system/ui/**'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
UI_JOB_NAME: "Create mici 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 }}-mici-raylib-ui"
|
||||
MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib"
|
||||
# All report files are pushed here
|
||||
REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports"
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
if: github.repository == 'commaai/openpilot'
|
||||
name: preview
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Waiting for ui generation to end
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ env.SHA }}
|
||||
check-name: ${{ env.UI_JOB_NAME }}
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed-conclusions: success
|
||||
wait-interval: 20
|
||||
|
||||
- name: Getting workflow run ID
|
||||
id: get_run_id
|
||||
run: |
|
||||
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Getting proposed ui # filename: pr_ui/mici_ui_replay.mp4
|
||||
id: download-artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run_id: ${{ steps.get_run_id.outputs.run_id }}
|
||||
search_artifacts: true
|
||||
name: mici-raylib-report-1-${{ env.REPORT_NAME }}
|
||||
path: ${{ github.workspace }}/pr_ui
|
||||
|
||||
- name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: commaai/ci-artifacts
|
||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
|
||||
path: ${{ github.workspace }}/master_ui_raylib
|
||||
ref: ${{ env.MASTER_BRANCH_NAME }}
|
||||
|
||||
- name: Saving new master ui
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}/master_ui_raylib
|
||||
run: |
|
||||
git checkout --orphan=new_master_ui_mici_raylib
|
||||
git rm -rf *
|
||||
git branch -D ${{ env.MASTER_BRANCH_NAME }}
|
||||
git branch -m ${{ env.MASTER_BRANCH_NAME }}
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
mv ${{ github.workspace }}/pr_ui/* .
|
||||
git add .
|
||||
git commit -m "mici raylib video for commit ${{ env.SHA }}"
|
||||
git push origin ${{ env.MASTER_BRANCH_NAME }} --force
|
||||
|
||||
- name: Setup FFmpeg
|
||||
uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae
|
||||
|
||||
- name: Finding diff
|
||||
if: github.event_name == 'pull_request_target'
|
||||
id: find_diff
|
||||
run: |
|
||||
# Find the video file from PR
|
||||
pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4"
|
||||
mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video"
|
||||
|
||||
master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4"
|
||||
mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video"
|
||||
|
||||
# Run report
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}"
|
||||
diff_exit_code=0
|
||||
python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$?
|
||||
|
||||
# Copy diff report files
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/
|
||||
|
||||
REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html"
|
||||
if [ $diff_exit_code -eq 0 ]; then
|
||||
DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)"
|
||||
else
|
||||
DIFF="❌ <strong>Videos differ!</strong> [View Diff Report]($REPORT_URL)"
|
||||
fi
|
||||
echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Saving proposed ui
|
||||
if: github.event_name == 'pull_request_target'
|
||||
working-directory: ${{ github.workspace }}/master_ui_raylib
|
||||
run: |
|
||||
# Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
git checkout --orphan=${{ env.BRANCH_NAME }}
|
||||
git rm -rf *
|
||||
mv ${{ github.workspace }}/pr_ui/* .
|
||||
git add .
|
||||
git commit -m "mici raylib video for PR #${{ github.event.number }}"
|
||||
git push origin ${{ env.BRANCH_NAME }} --force
|
||||
|
||||
# Append diff report to report files branch
|
||||
git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
git checkout ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html
|
||||
git add diff_pr_${{ github.event.number }}.html
|
||||
git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit"
|
||||
git push origin ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
|
||||
- name: Comment Video on PR
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
message: |
|
||||
<!-- _(run_id_video_mici_raylib **${{ github.run_id }}**)_ -->
|
||||
## mici raylib UI Preview
|
||||
${{ steps.find_diff.outputs.DIFF }}
|
||||
comment_tag: run_id_video_mici_raylib
|
||||
pr_number: ${{ github.event.number }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
54
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
54
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
@@ -6,10 +6,10 @@ env:
|
||||
CI_DIR: ${{ github.workspace }}/release/ci
|
||||
SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache
|
||||
PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot"
|
||||
|
||||
|
||||
# Branch configurations
|
||||
STAGING_SOURCE_BRANCH: 'master'
|
||||
|
||||
|
||||
# Runtime configuration
|
||||
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
cancel="$(echo "$CONFIG" | jq -r '.cancel_publish_in_progress')";
|
||||
echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT
|
||||
echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')";
|
||||
echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
fi
|
||||
echo "build=$BUILD" >> $GITHUB_OUTPUT
|
||||
cat $GITHUB_OUTPUT
|
||||
|
||||
|
||||
validate_tests:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [ prepare_strategy ]
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
needs.prepare_strategy.result == 'success' &&
|
||||
(needs.validate_tests.result == 'success' || needs.validate_tests.result == 'skipped') &&
|
||||
(!contains(github.event_name, 'pull_request') ||
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
with:
|
||||
path: ${{env.SCONS_CACHE_DIR}}
|
||||
key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}-${{ github.sha }}
|
||||
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
|
||||
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
|
||||
# for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden.
|
||||
restore-keys: |
|
||||
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT
|
||||
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Set up common environment
|
||||
source /etc/profile;
|
||||
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
|
||||
@@ -180,15 +180,6 @@ jobs:
|
||||
./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/
|
||||
cd $BUILD_DIR
|
||||
sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py
|
||||
echo "Building sunnypilot's modeld..."
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld
|
||||
echo "Building sunnypilot's modeld_v2..."
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld_v2
|
||||
echo "Building sunnypilot's locationd..."
|
||||
scons -j2 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/selfdrive/locationd
|
||||
echo "Building openpilot's locationd..."
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal selfdrive/locationd
|
||||
echo "Building rest of sunnypilot"
|
||||
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal
|
||||
touch ${BUILD_DIR}/prebuilt
|
||||
if [[ "${{ runner.debug }}" == "1" ]]; then
|
||||
@@ -200,28 +191,37 @@ jobs:
|
||||
sudo rm -rf ${OUTPUT_DIR}
|
||||
mkdir -p ${OUTPUT_DIR}
|
||||
rsync -am${RUNNER_DEBUG:+v} \
|
||||
--include='**/panda/board/' \
|
||||
--include='**/panda/board/obj' \
|
||||
--include='**/panda/board/obj/panda.bin.signed' \
|
||||
--include='**/panda/board/obj/panda_h7.bin.signed' \
|
||||
--include='**/panda/board/obj/bootstub.panda.bin' \
|
||||
--include='**/panda/board/obj/bootstub.panda_h7.bin' \
|
||||
--exclude='.sconsign.dblite' \
|
||||
--exclude='*.a' \
|
||||
--exclude='*.o' \
|
||||
--exclude='*.os' \
|
||||
--exclude='*.pyc' \
|
||||
--exclude='moc_*' \
|
||||
--exclude='__pycache__' \
|
||||
--exclude='*.cc' \
|
||||
--exclude='Jenkinsfile' \
|
||||
--exclude='supercombo.onnx' \
|
||||
--exclude='**/panda/board/*' \
|
||||
--exclude='**/panda/board/obj/**' \
|
||||
--exclude='**/panda/certs/' \
|
||||
--exclude='**/panda/crypto/' \
|
||||
--exclude='**/release/' \
|
||||
--exclude='**/.github/' \
|
||||
--exclude='**/selfdrive/ui/replay/' \
|
||||
--exclude='**/__pycache__/' \
|
||||
--exclude='**/selfdrive/ui/*.h' \
|
||||
--exclude='**/selfdrive/ui/**/*.h' \
|
||||
--exclude='**/selfdrive/ui/qt/offroad/sunnypilot/' \
|
||||
--exclude='${{env.SCONS_CACHE_DIR}}' \
|
||||
--exclude='**/.git/' \
|
||||
--exclude='**/SConstruct' \
|
||||
--exclude='**/SConscript' \
|
||||
--exclude='**/.venv/' \
|
||||
--exclude='selfdrive/modeld/models/driving_vision.onnx' \
|
||||
--exclude='selfdrive/modeld/models/driving_policy.onnx' \
|
||||
--exclude='sunnypilot/modeld*/models/supercombo.onnx' \
|
||||
--exclude='third_party/*x86*' \
|
||||
--exclude='third_party/*Darwin*' \
|
||||
--delete-excluded \
|
||||
--chown=comma:comma \
|
||||
${BUILD_DIR}/ ${OUTPUT_DIR}/
|
||||
@@ -241,8 +241,8 @@ jobs:
|
||||
if: always()
|
||||
run: |
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
|
||||
|
||||
|
||||
|
||||
|
||||
publish:
|
||||
concurrency:
|
||||
# We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name.
|
||||
@@ -293,7 +293,7 @@ jobs:
|
||||
echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES"
|
||||
echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}"
|
||||
echo "3. Update as needed (JSON array with no spaces)"
|
||||
|
||||
|
||||
- name: Tag ${{ needs.prepare_strategy.outputs.environment }}
|
||||
if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }}
|
||||
run: |
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
git push -f origin ${TAG}
|
||||
|
||||
notify:
|
||||
needs:
|
||||
needs:
|
||||
- prepare_strategy
|
||||
- build
|
||||
- publish
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}
|
||||
EOF
|
||||
)
|
||||
|
||||
|
||||
{
|
||||
echo 'content<<EOFMARKER'
|
||||
echo "$MESSAGE"
|
||||
|
||||
29
.github/workflows/tests.yaml
vendored
29
.github/workflows/tests.yaml
vendored
@@ -21,11 +21,12 @@ env:
|
||||
PYTHONWARNINGS: error
|
||||
BASE_IMAGE: sunnypilot-base
|
||||
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
|
||||
MAPBOX_TOKEN_CI: ${{ secrets.MAPBOX_TOKEN_CI }}
|
||||
|
||||
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD: release/ci/docker_build_sp.sh base
|
||||
|
||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -e MAPBOX_TOKEN_CI=$MAPBOX_TOKEN_CI -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||
|
||||
@@ -297,29 +298,3 @@ jobs:
|
||||
with:
|
||||
name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||
path: selfdrive/ui/tests/test_ui/raylib_report/screenshots
|
||||
|
||||
create_mici_raylib_ui_report:
|
||||
name: Create mici 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"]') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: ./.github/workflows/setup-with-retry
|
||||
- name: Build openpilot
|
||||
run: ${{ env.RUN }} "scons -j$(nproc)"
|
||||
- name: Create mici raylib UI Report
|
||||
run: >
|
||||
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
|
||||
source selfdrive/test/setup_xvfb.sh &&
|
||||
WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py"
|
||||
- name: Upload Raylib UI Report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||
path: selfdrive/ui/tests/diff/report
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
Version 0.10.3 (2025-12-10)
|
||||
========================
|
||||
|
||||
Version 0.10.2 (2025-11-19)
|
||||
========================
|
||||
* comma four support
|
||||
|
||||
10
SConstruct
10
SConstruct
@@ -75,6 +75,7 @@ env = Environment(
|
||||
"#third_party/acados/include/hpipm/include",
|
||||
"#third_party/catch2/include",
|
||||
"#third_party/libyuv/include",
|
||||
"#third_party/snpe/include",
|
||||
],
|
||||
LIBPATH=[
|
||||
"#common",
|
||||
@@ -100,6 +101,7 @@ if arch == "larch64":
|
||||
"/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)
|
||||
@@ -123,6 +125,14 @@ else:
|
||||
"/usr/local/lib",
|
||||
])
|
||||
|
||||
if arch == "x86_64":
|
||||
env.Append(LIBPATH=[
|
||||
f"#third_party/snpe/{arch}"
|
||||
])
|
||||
env.Append(RPATH=[
|
||||
Dir(f"#third_party/snpe/{arch}").abspath,
|
||||
])
|
||||
|
||||
# Sanitizers and extra CCFLAGS from CLI
|
||||
if GetOption('asan'):
|
||||
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])
|
||||
|
||||
@@ -192,7 +192,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
aTarget @5 :Float32;
|
||||
events @6 :List(OnroadEventSP.Event);
|
||||
e2eAlerts @7 :E2eAlerts;
|
||||
accelPersonality @8 :AccelerationPersonality;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
@@ -204,11 +203,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
blended @1;
|
||||
}
|
||||
}
|
||||
enum AccelerationPersonality {
|
||||
sport @0;
|
||||
normal @1;
|
||||
eco @2;
|
||||
}
|
||||
|
||||
struct SmartCruiseControl {
|
||||
vision @0 :Vision;
|
||||
map @1 :Map;
|
||||
|
||||
@@ -22,5 +22,5 @@ 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() -> tuple[str, str, str] | tuple[None, None, None]:
|
||||
def get_key_pair():
|
||||
return CommaConnectApi(None).get_key_pair()
|
||||
|
||||
@@ -6,9 +6,9 @@ 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"}
|
||||
# name : jwt signature algorithm
|
||||
KEYS = {"id_rsa" : "RS256",
|
||||
"id_ecdsa" : "ES256"}
|
||||
|
||||
|
||||
class BaseApi:
|
||||
@@ -62,7 +62,7 @@ class BaseApi:
|
||||
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
|
||||
|
||||
@staticmethod
|
||||
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
|
||||
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:
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "Dark Souls 2 (Default)"
|
||||
#define DEFAULT_MODEL "The Cool People (Default)"
|
||||
|
||||
@@ -71,7 +71,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LastGPSPosition", {PERSISTENT, STRING}},
|
||||
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
|
||||
{"LastAgnosPowerMonitorShutdown", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastUpdateRouteCount", {PERSISTENT, INT, "0"}},
|
||||
@@ -133,8 +132,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
||||
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
@@ -192,6 +189,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
||||
|
||||
// Navigation params
|
||||
{"MapboxToken", {PERSISTENT | BACKUP, STRING}},
|
||||
{"MapboxSettings", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"MapboxRoute", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
|
||||
// Neural Network Lateral Control
|
||||
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
from openpilot.common.utils import atomic_write
|
||||
from openpilot.common.utils import atomic_write_in_dir
|
||||
|
||||
|
||||
class TestFileHelpers:
|
||||
@@ -15,5 +15,5 @@ class TestFileHelpers:
|
||||
assert f.read() == "test"
|
||||
os.remove(path)
|
||||
|
||||
def test_atomic_write(self):
|
||||
self.run_atomic_write_func(atomic_write)
|
||||
def test_atomic_write_in_dir(self):
|
||||
self.run_atomic_write_func(atomic_write_in_dir)
|
||||
|
||||
@@ -32,8 +32,8 @@ class CallbackReader:
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None,
|
||||
overwrite: bool = False):
|
||||
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)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define COMMA_VERSION "0.10.3"
|
||||
#define COMMA_VERSION "0.10.2"
|
||||
|
||||
@@ -20,7 +20,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2017">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EV Non-ACC 2018-21|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2018-21">Buy Here</a></sub></details>|||
|
||||
@@ -384,7 +384,7 @@ If your car has the following packages or features, then it's a good candidate f
|
||||
|
||||
| Make | Required Package/Features |
|
||||
| ---- | ------------------------- |
|
||||
| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. |
|
||||
| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. |
|
||||
| Ford | Any car with Lane Centering will likely work. |
|
||||
| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. |
|
||||
| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. |
|
||||
|
||||
@@ -31,7 +31,7 @@ We'll run the `replay` tool with the demo route to get data streaming for testin
|
||||
tools/replay/replay --demo
|
||||
|
||||
# in terminal 2
|
||||
./selfdrive/ui/ui.py
|
||||
selfdrive/ui/ui
|
||||
```
|
||||
|
||||
The openpilot UI should launch and show a replay of the demo route.
|
||||
@@ -43,36 +43,39 @@ If you have your own comma device, you can replace `--demo` with one of your own
|
||||
|
||||
Now let’s update the speed display color in the UI.
|
||||
|
||||
Search for the function responsible for rendering the current speed:
|
||||
Search for the function responsible for rendering UI text:
|
||||
```bash
|
||||
git grep "_draw_current_speed" selfdrive/ui/onroad/hud_renderer.py
|
||||
git grep "drawText" selfdrive/ui/qt/onroad/hud.cc
|
||||
```
|
||||
|
||||
You'll find the relevant code inside `selfdrive/ui/onroad/hud_renderer.py`, in this function:
|
||||
You’ll find the relevant code inside `selfdrive/ui/qt/onroad/hud.cc`, in this function:
|
||||
|
||||
```python
|
||||
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the current vehicle speed and unit."""
|
||||
speed_text = str(round(self.speed))
|
||||
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) # <- this sets the speed text color
|
||||
```cpp
|
||||
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)); // <- this sets the speed text color
|
||||
p.drawText(real_rect.x(), real_rect.bottom(), text);
|
||||
}
|
||||
```
|
||||
|
||||
Change `COLORS.white` to make it **blue** instead of white. A nice soft blue is `#8080FF`, which you can change inline:
|
||||
Change the `QColor(...)` line to make it **blue** instead of white. A nice soft blue is `#8080FF`, which translates to:
|
||||
|
||||
```diff
|
||||
- rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
+ rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, rl.Color(0x80, 0x80, 0xFF, 255))
|
||||
- p.setPen(QColor(0xff, 0xff, 0xff, alpha));
|
||||
+ p.setPen(QColor(0x80, 0x80, 0xFF, alpha));
|
||||
```
|
||||
|
||||
This change will tint all speed-related UI text to blue with the same transparency (`alpha`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Re-run the UI
|
||||
## 4. Rebuild the UI
|
||||
|
||||
After making changes, re-run the UI to see your new UI:
|
||||
After making changes, rebuild Openpilot so your new UI is compiled:
|
||||
```bash
|
||||
./selfdrive/ui/ui.py
|
||||
scons -j$(nproc) && selfdrive/ui/ui
|
||||
```
|
||||

|
||||
|
||||
|
||||
Submodule opendbc_repo updated: f746884333...61bf5a90c5
2
panda
2
panda
Submodule panda updated: 5f3c09c910...dee9061b2a
@@ -85,7 +85,6 @@ docs = [
|
||||
]
|
||||
|
||||
testing = [
|
||||
"coverage",
|
||||
"hypothesis ==6.47.*",
|
||||
"mypy",
|
||||
"pytest",
|
||||
@@ -116,7 +115,7 @@ dev = [
|
||||
"pyautogui",
|
||||
"pygame",
|
||||
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
|
||||
"pytools>=2025.1.6; platform_machine != 'aarch64'",
|
||||
"pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version
|
||||
"pywinctl",
|
||||
"pyprof2calltree",
|
||||
"tabulate",
|
||||
@@ -126,7 +125,7 @@ dev = [
|
||||
|
||||
tools = [
|
||||
"metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')",
|
||||
"dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64
|
||||
"dearpygui>=2.1.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -227,7 +226,7 @@ lint.select = [
|
||||
"TRY203", "TRY400", "TRY401", # try/excepts
|
||||
"RUF008", "RUF100",
|
||||
"TID251",
|
||||
"PLE", "PLR1704",
|
||||
"PLR1704",
|
||||
]
|
||||
lint.ignore = [
|
||||
"E741",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
cd $DIR/../../tinygrad_repo
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
|
||||
#export DEBUG=2
|
||||
export PYTHONPATH=.
|
||||
export AM_RESET=1
|
||||
export AMD=1
|
||||
export AMD_IFACE=USB
|
||||
export AMD_LLVM=1
|
||||
|
||||
python3 -m unittest -q --buffer test.test_tiny.TestTiny.test_plus \
|
||||
> /tmp/test_tiny.log 2>&1 || (cat /tmp/test_tiny.log; exit 1)
|
||||
printf "${GREEN}Booted in ${SECONDS}s${NC}\n"
|
||||
printf "${GREEN}=============${NC}\n"
|
||||
|
||||
printf "\n\n"
|
||||
printf "${GREEN}Transfer speeds:${NC}\n"
|
||||
printf "${GREEN}================${NC}\n"
|
||||
python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
import numpy as np
|
||||
from scipy.io import wavfile
|
||||
|
||||
|
||||
sr = 48000
|
||||
max_int16 = 2**15 - 1
|
||||
|
||||
def harmonic_beep(freq, duration_seconds):
|
||||
n_total = int(sr * duration_seconds)
|
||||
|
||||
signal = np.sin(2 * np.pi * freq * np.arange(n_total) / sr)
|
||||
x = np.arange(n_total)
|
||||
exp_scale = np.exp(-x/5.5e3)
|
||||
return max_int16 * signal * exp_scale
|
||||
|
||||
engage_beep = harmonic_beep(1661.219, 0.5)
|
||||
wavfile.write("engage.wav", sr, engage_beep.astype(np.int16))
|
||||
disengage_beep = harmonic_beep(1318.51, 0.5)
|
||||
wavfile.write("disengage.wav", sr, disengage_beep.astype(np.int16))
|
||||
@@ -42,7 +42,7 @@ If your car has the following packages or features, then it's a good candidate f
|
||||
|
||||
| Make | Required Package/Features |
|
||||
| ---- | ------------------------- |
|
||||
| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. |
|
||||
| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. |
|
||||
| Ford | Any car with Lane Centering will likely work. |
|
||||
| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. |
|
||||
| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. |
|
||||
|
||||
@@ -26,18 +26,6 @@ class MockCarState:
|
||||
return CS, CS_SP
|
||||
|
||||
|
||||
BRAND_EXTRA_GEARS = {
|
||||
'ford': [GearShifter.low, GearShifter.manumatic],
|
||||
'nissan': [GearShifter.brake],
|
||||
'chrysler': [GearShifter.low],
|
||||
'honda': [GearShifter.sport],
|
||||
'toyota': [GearShifter.sport],
|
||||
'gm': [GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic],
|
||||
'volkswagen': [GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
|
||||
'hyundai': [GearShifter.sport, GearShifter.manumatic]
|
||||
}
|
||||
|
||||
|
||||
class CarSpecificEvents:
|
||||
def __init__(self, CP: structs.CarParams):
|
||||
self.CP = CP
|
||||
@@ -48,13 +36,17 @@ class CarSpecificEvents:
|
||||
self.silent_steer_warning = True
|
||||
|
||||
def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl):
|
||||
extra_gears = BRAND_EXTRA_GEARS.get(self.CP.brand, None)
|
||||
|
||||
if self.CP.brand in ('body', 'mock'):
|
||||
events = Events()
|
||||
|
||||
elif self.CP.brand == 'ford':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low, GearShifter.manumatic])
|
||||
|
||||
elif self.CP.brand == 'nissan':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.brake])
|
||||
|
||||
elif self.CP.brand == 'chrysler':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low])
|
||||
|
||||
# Low speed steer alert hysteresis logic
|
||||
if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5):
|
||||
@@ -65,7 +57,7 @@ class CarSpecificEvents:
|
||||
events.add(EventName.belowSteerSpeed)
|
||||
|
||||
elif self.CP.brand == 'honda':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=False)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport], pcm_enable=False)
|
||||
|
||||
if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed:
|
||||
events.add(EventName.belowEngageSpeed)
|
||||
@@ -87,7 +79,7 @@ class CarSpecificEvents:
|
||||
|
||||
elif self.CP.brand == 'toyota':
|
||||
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport])
|
||||
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
if CS.cruiseState.standstill and not CS.brakePressed:
|
||||
@@ -102,7 +94,9 @@ class CarSpecificEvents:
|
||||
events.add(EventName.manualRestart)
|
||||
|
||||
elif self.CP.brand == 'gm':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport, GearShifter.low,
|
||||
GearShifter.eco, GearShifter.manumatic],
|
||||
pcm_enable=self.CP.pcmCruise)
|
||||
|
||||
# Enabling at a standstill with brake is allowed
|
||||
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs
|
||||
@@ -113,7 +107,8 @@ class CarSpecificEvents:
|
||||
events.add(EventName.resumeRequired)
|
||||
|
||||
elif self.CP.brand == 'volkswagen':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
|
||||
pcm_enable=self.CP.pcmCruise)
|
||||
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
if CS.vEgo < self.CP.minEnableSpeed + 0.5:
|
||||
@@ -126,14 +121,15 @@ class CarSpecificEvents:
|
||||
# events.add(EventName.steerTimeLimit)
|
||||
|
||||
elif self.CP.brand == 'hyundai':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=(GearShifter.sport, GearShifter.manumatic),
|
||||
pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
|
||||
|
||||
else:
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
||||
events = self.create_common_events(CS, CS_prev)
|
||||
|
||||
return events
|
||||
|
||||
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears: list | None = None, pcm_enable=True,
|
||||
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears=None, pcm_enable=True,
|
||||
allow_button_cancel=True):
|
||||
events = Events()
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
from numbers import Number
|
||||
|
||||
from cereal import car, log
|
||||
@@ -20,6 +22,8 @@ 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
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.controlsd_ext import ControlsExt
|
||||
|
||||
State = log.SelfdriveState.OpenpilotState
|
||||
@@ -29,7 +33,7 @@ LaneChangeDirection = log.LaneChangeDirection
|
||||
ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys())
|
||||
|
||||
|
||||
class Controls(ControlsExt):
|
||||
class Controls(ControlsExt, ModelStateBase):
|
||||
def __init__(self) -> None:
|
||||
self.params = Params()
|
||||
cloudlog.info("controlsd is waiting for CarParams")
|
||||
@@ -38,6 +42,7 @@ class Controls(ControlsExt):
|
||||
|
||||
# Initialize sunnypilot controlsd extension and base model state
|
||||
ControlsExt.__init__(self, self.CP, self.params)
|
||||
ModelStateBase.__init__(self)
|
||||
|
||||
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
||||
|
||||
@@ -226,15 +231,30 @@ class Controls(ControlsExt):
|
||||
cc_send.carControl = CC
|
||||
self.pm.send('carControl', cc_send)
|
||||
|
||||
def params_thread(self, evt):
|
||||
while not evt.is_set():
|
||||
self.get_params_sp()
|
||||
|
||||
if self.CP.lateralTuning.which() == 'torque':
|
||||
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
def run(self):
|
||||
rk = Ratekeeper(100, print_delay_threshold=None)
|
||||
while True:
|
||||
self.update()
|
||||
CC, lac_log = self.state_control()
|
||||
self.publish(CC, lac_log)
|
||||
self.get_params_sp(self.sm)
|
||||
self.run_ext(self.sm, self.pm)
|
||||
rk.monitor_time()
|
||||
e = threading.Event()
|
||||
t = threading.Thread(target=self.params_thread, args=(e,))
|
||||
try:
|
||||
t.start()
|
||||
while True:
|
||||
self.update()
|
||||
CC, lac_log = self.state_control()
|
||||
self.publish(CC, lac_log)
|
||||
self.run_ext(self.sm, self.pm)
|
||||
rk.monitor_time()
|
||||
finally:
|
||||
e.set()
|
||||
t.join()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -22,17 +22,15 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La
|
||||
# Additionally, there is friction in the steering wheel that needs
|
||||
# to be overcome to move it at all, this is compensated for too.
|
||||
|
||||
KP = 0.8
|
||||
KI = 0.15
|
||||
|
||||
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
|
||||
JERK_LOOKAHEAD_SECONDS = 0.19
|
||||
JERK_GAIN = 0.3
|
||||
LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0
|
||||
VERSION = 1
|
||||
VERSION = 0
|
||||
|
||||
class LatControlTorque(LatControl):
|
||||
def __init__(self, CP, CP_SP, CI, dt):
|
||||
@@ -40,13 +38,13 @@ class LatControlTorque(LatControl):
|
||||
self.torque_params = CP.lateralTuning.torque.as_builder()
|
||||
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
||||
self.lateral_accel_from_torque = CI.lateral_accel_from_torque()
|
||||
self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, rate=1/self.dt)
|
||||
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.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt)
|
||||
self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
|
||||
self.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)
|
||||
|
||||
@@ -78,15 +76,17 @@ class LatControlTorque(LatControl):
|
||||
|
||||
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]
|
||||
lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2))
|
||||
raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt)
|
||||
desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk)
|
||||
# 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
|
||||
setpoint = expected_lateral_accel
|
||||
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
|
||||
@@ -94,10 +94,15 @@ class LatControlTorque(LatControl):
|
||||
ff = gravity_adjusted_future_lateral_accel
|
||||
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
||||
ff -= self.torque_params.latAccelOffset
|
||||
ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
# 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, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator)
|
||||
output_lataccel = self.pid.update(pid_log.error,
|
||||
-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
|
||||
|
||||
@@ -10,8 +10,6 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.modeld.constants import index_function
|
||||
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
||||
|
||||
if __name__ == '__main__': # generating code
|
||||
from openpilot.third_party.acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
|
||||
else:
|
||||
@@ -230,7 +228,6 @@ class LongitudinalMpc:
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = SOURCES[2]
|
||||
self.accel_controller = AccelPersonalityController()
|
||||
|
||||
def reset(self):
|
||||
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
@@ -335,13 +332,6 @@ class LongitudinalMpc:
|
||||
v_ego = self.x0[1]
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
|
||||
|
||||
if self.accel_controller.is_enabled():
|
||||
min_accel = self.accel_controller.get_min_accel(v_ego)
|
||||
else:
|
||||
min_accel = CRUISE_MIN_ACCEL
|
||||
a_cruise_min = min_accel
|
||||
|
||||
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
||||
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
||||
|
||||
@@ -360,7 +350,7 @@ class LongitudinalMpc:
|
||||
|
||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||
# when the leads are no factor.
|
||||
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
|
||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||
# TODO does this make sense when max_a is negative?
|
||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
|
||||
|
||||
@@ -124,11 +124,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
if mode == 'acc':
|
||||
if self.accel_controller.is_enabled():
|
||||
max_accel = self.accel_controller.get_max_accel(v_ego)
|
||||
accel_clip = [ACCEL_MIN, max_accel]
|
||||
else:
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
else:
|
||||
|
||||
@@ -172,7 +172,7 @@ class PoseCalibrator:
|
||||
ned_from_calib_euler = self._ned_from_calib(pose.orientation)
|
||||
angular_velocity_calib = self._transform_calib_from_device(pose.angular_velocity)
|
||||
acceleration_calib = self._transform_calib_from_device(pose.acceleration)
|
||||
velocity_calib = self._transform_calib_from_device(pose.velocity)
|
||||
velocity_calib = self._transform_calib_from_device(pose.angular_velocity)
|
||||
|
||||
return Pose(ned_from_calib_euler, velocity_calib, acceleration_calib, angular_velocity_calib)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
102
selfdrive/modeld/tests/test_modeld.py
Normal file
102
selfdrive/modeld/tests/test_modeld.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state
|
||||
|
||||
CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam
|
||||
IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8)
|
||||
IMG_BYTES = IMG.flatten().tobytes()
|
||||
|
||||
|
||||
class TestModeld:
|
||||
|
||||
def setup_method(self):
|
||||
self.vipc_server = VisionIpcServer("camerad")
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.start_listener()
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes())
|
||||
|
||||
self.sm = messaging.SubMaster(['modelV2', 'cameraOdometry'])
|
||||
self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'liveCalibration'])
|
||||
|
||||
managed_processes['modeld'].start()
|
||||
self.pm.wait_for_readers_to_update("roadCameraState", 10)
|
||||
|
||||
def teardown_method(self):
|
||||
managed_processes['modeld'].stop()
|
||||
del self.vipc_server
|
||||
|
||||
def _send_frames(self, frame_id, cams=None):
|
||||
if cams is None:
|
||||
cams = ('roadCameraState', 'wideRoadCameraState')
|
||||
|
||||
cs = None
|
||||
for cam in cams:
|
||||
msg = messaging.new_message(cam)
|
||||
cs = getattr(msg, cam)
|
||||
cs.frameId = frame_id
|
||||
cs.timestampSof = int((frame_id * DT_MDL) * 1e9)
|
||||
cs.timestampEof = int(cs.timestampSof + (DT_MDL * 1e9))
|
||||
cam_meta = meta_from_camera_state(cam)
|
||||
|
||||
self.pm.send(msg.which(), msg)
|
||||
self.vipc_server.send(cam_meta.stream, IMG_BYTES, cs.frameId,
|
||||
cs.timestampSof, cs.timestampEof)
|
||||
return cs
|
||||
|
||||
def _wait(self):
|
||||
self.sm.update(5000)
|
||||
if self.sm['modelV2'].frameId != self.sm['cameraOdometry'].frameId:
|
||||
self.sm.update(1000)
|
||||
|
||||
def test_modeld(self):
|
||||
for n in range(1, 500):
|
||||
cs = self._send_frames(n)
|
||||
self._wait()
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
assert mdl.frameId == n
|
||||
assert mdl.frameIdExtra == n
|
||||
assert mdl.timestampEof == cs.timestampEof
|
||||
assert mdl.frameAge == 0
|
||||
assert mdl.frameDropPerc == 0
|
||||
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert odo.frameId == n
|
||||
assert odo.timestampEof == cs.timestampEof
|
||||
|
||||
def test_dropped_frames(self):
|
||||
"""
|
||||
modeld should only run on consecutive road frames
|
||||
"""
|
||||
frame_id = -1
|
||||
road_frames = list()
|
||||
for n in range(1, 50):
|
||||
if (random.random() < 0.1) and n > 3:
|
||||
cams = random.choice([(), ('wideRoadCameraState', )])
|
||||
self._send_frames(n, cams)
|
||||
else:
|
||||
self._send_frames(n)
|
||||
road_frames.append(n)
|
||||
self._wait()
|
||||
|
||||
if len(road_frames) < 3 or road_frames[-1] - road_frames[-2] == 1:
|
||||
frame_id = road_frames[-1]
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert mdl.frameId == frame_id
|
||||
assert mdl.frameIdExtra == frame_id
|
||||
assert odo.frameId == frame_id
|
||||
if n != frame_id:
|
||||
assert not self.sm.updated['modelV2']
|
||||
assert not self.sm.updated['cameraOdometry']
|
||||
@@ -40,8 +40,8 @@ def dmonitoringd_thread():
|
||||
|
||||
# save rhd virtual toggle every 5 mins
|
||||
if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and
|
||||
DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
|
||||
DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
|
||||
DM.wheelpos_learner.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
|
||||
DM.wheel_on_right == (DM.wheelpos_learner.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
|
||||
params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
|
||||
|
||||
def main():
|
||||
|
||||
@@ -40,9 +40,6 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._PHONE_THRESH2 = 15.0
|
||||
self._PHONE_MAX_OFFSET = 0.06
|
||||
self._PHONE_MIN_OFFSET = 0.025
|
||||
self._PHONE_DATA_AVG = 0.05
|
||||
self._PHONE_DATA_VAR = 3*0.005
|
||||
self._PHONE_MAX_COUNT = int(360 / self._DT_DMON)
|
||||
|
||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||
@@ -50,11 +47,9 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._POSE_YAW_THRESHOLD = 0.4020
|
||||
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
|
||||
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
|
||||
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
|
||||
self._PITCH_NATURAL_OFFSET = 0.029 # initial value before offset is learned
|
||||
self._PITCH_NATURAL_THRESHOLD = 0.449
|
||||
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
|
||||
self._PITCH_NATURAL_VAR = 3*0.01
|
||||
self._YAW_NATURAL_VAR = 3*0.05
|
||||
self._YAW_NATURAL_OFFSET = 0.097 # initial value before offset is learned
|
||||
self._PITCH_MAX_OFFSET = 0.124
|
||||
self._PITCH_MIN_OFFSET = -0.0881
|
||||
self._YAW_MAX_OFFSET = 0.289
|
||||
@@ -75,9 +70,6 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._WHEELPOS_CALIB_MIN_SPEED = 11
|
||||
self._WHEELPOS_THRESHOLD = 0.5
|
||||
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side
|
||||
self._WHEELPOS_DATA_AVG = 0.03
|
||||
self._WHEELPOS_DATA_VAR = 3*5.5e-5
|
||||
self._WHEELPOS_MAX_COUNT = -1
|
||||
|
||||
self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change
|
||||
self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change
|
||||
@@ -86,33 +78,30 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts
|
||||
|
||||
class DistractedType:
|
||||
|
||||
NOT_DISTRACTED = 0
|
||||
DISTRACTED_POSE = 1 << 0
|
||||
DISTRACTED_BLINK = 1 << 1
|
||||
DISTRACTED_PHONE = 1 << 2
|
||||
|
||||
class DriverPose:
|
||||
def __init__(self, settings):
|
||||
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
|
||||
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
|
||||
def __init__(self, max_trackable):
|
||||
self.yaw = 0.
|
||||
self.pitch = 0.
|
||||
self.roll = 0.
|
||||
self.yaw_std = 0.
|
||||
self.pitch_std = 0.
|
||||
self.roll_std = 0.
|
||||
self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable)
|
||||
self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable)
|
||||
self.calibrated = False
|
||||
self.low_std = True
|
||||
self.cfactor_pitch = 1.
|
||||
self.cfactor_yaw = 1.
|
||||
|
||||
class DriverProb:
|
||||
def __init__(self, raw_priors, max_trackable):
|
||||
class DriverPhone:
|
||||
def __init__(self, max_trackable):
|
||||
self.prob = 0.
|
||||
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
|
||||
self.prob_offseter = RunningStatFilter(max_trackable=max_trackable)
|
||||
self.prob_calibrated = False
|
||||
|
||||
class DriverBlink:
|
||||
@@ -151,11 +140,9 @@ class DriverMonitoring:
|
||||
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
|
||||
|
||||
# init driver status
|
||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||
phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2)
|
||||
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||
self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT)
|
||||
self.pose = DriverPose(settings=self.settings)
|
||||
self.wheelpos_learner = RunningStatFilter()
|
||||
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.phone = DriverPhone(self.settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.blink = DriverBlink()
|
||||
|
||||
self.always_on = always_on
|
||||
@@ -247,11 +234,8 @@ class DriverMonitoring:
|
||||
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
|
||||
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
|
||||
yaw_error = abs(yaw_error)
|
||||
|
||||
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
|
||||
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
|
||||
|
||||
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
|
||||
if pitch_error > (self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD) or \
|
||||
yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw:
|
||||
distracted_types.append(DistractedType.DISTRACTED_POSE)
|
||||
|
||||
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
||||
@@ -272,12 +256,9 @@ class DriverMonitoring:
|
||||
# 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
|
||||
driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD):
|
||||
self.wheelpos.prob_offseter.push_and_update(rhd_pred)
|
||||
|
||||
self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT
|
||||
|
||||
if self.wheelpos.prob_calibrated or demo_mode:
|
||||
self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
|
||||
self.wheelpos_learner.push_and_update(rhd_pred)
|
||||
if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT or demo_mode:
|
||||
self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
|
||||
else:
|
||||
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
|
||||
# make sure no switching when engaged
|
||||
|
||||
@@ -1 +1 @@
|
||||
e0ad86508edb61b3eaa1b84662c515d2c3368295
|
||||
b508f43fb0481bce0859c9b6ab4f45ee690b8dab
|
||||
@@ -206,9 +206,8 @@ class TestOnroad:
|
||||
result += "-------------- UI Draw Timing ------------------\n"
|
||||
result += "------------------------------------------------\n"
|
||||
|
||||
# other processes preempt ui while starting up
|
||||
offset = int(20 * LOG_OFFSET)
|
||||
ts = self.ts['uiDebug']['drawTimeMillis'][offset:]
|
||||
# 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"
|
||||
|
||||
@@ -227,6 +227,6 @@ class HomeLayout(Widget):
|
||||
self._prev_alerts_present = alerts_present
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "sunnypilot"
|
||||
brand = "openpilot"
|
||||
description = self.params.get("UpdaterCurrentDescription")
|
||||
return f"{brand} {description}" if description else brand
|
||||
|
||||
@@ -10,9 +10,6 @@ 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
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
HOME = 0
|
||||
|
||||
@@ -109,8 +109,8 @@ class TermsPage(Widget):
|
||||
self._on_accept = on_accept
|
||||
self._on_decline = on_decline
|
||||
|
||||
self._title = Label(tr("Welcome to sunnypilot"), 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 sunnypilot. Read the latest terms at https://comma.ai/terms before continuing."),
|
||||
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)
|
||||
@@ -143,10 +143,10 @@ class TermsPage(Widget):
|
||||
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 sunnypilot."),
|
||||
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 sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER,
|
||||
click_callback=self._on_uninstall_clicked)
|
||||
|
||||
def _on_uninstall_clicked(self):
|
||||
|
||||
@@ -23,11 +23,10 @@ DESCRIPTIONS = {
|
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||
),
|
||||
'alpha_longitudinal': tr_noop(
|
||||
"<b>WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
|
||||
"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. " +
|
||||
"Changing this setting will restart sunnypilot if the car is powered on."
|
||||
"<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
|
||||
"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."
|
||||
),
|
||||
}
|
||||
|
||||
@@ -72,7 +71,7 @@ class DeveloperLayout(Widget):
|
||||
)
|
||||
|
||||
self._alpha_long_toggle = toggle_item(
|
||||
lambda: tr("sunnypilot Longitudinal Control (Alpha)"),
|
||||
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,
|
||||
@@ -149,7 +148,6 @@ class DeveloperLayout(Widget):
|
||||
self._params.put_bool("ShowDebugInfo", state)
|
||||
gui_app.set_show_touches(state)
|
||||
gui_app.set_show_fps(state)
|
||||
gui_app.set_show_mouse_coords(state)
|
||||
|
||||
def _on_enable_adb(self, state: bool):
|
||||
self._params.put_bool("AdbEnabled", state)
|
||||
|
||||
@@ -19,15 +19,12 @@ from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_b
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'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("sunnypilot 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 sunnypilot"),
|
||||
'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"),
|
||||
}
|
||||
|
||||
|
||||
@@ -165,8 +162,8 @@ class DeviceLayout(Widget):
|
||||
cloudlog.exception("invalid LiveTorqueParameters")
|
||||
|
||||
desc += "<br><br>"
|
||||
desc += tr("sunnypilot is continuously calibrating, resetting is rarely required. " +
|
||||
"Resetting calibration will restart sunnypilot if the car is powered on.")
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import pyray as rl
|
||||
import time
|
||||
import threading
|
||||
|
||||
from openpilot.common.api import api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, 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.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutBase
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
"sunnypilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
"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."
|
||||
)
|
||||
@@ -24,17 +32,50 @@ INSTRUCTIONS = tr_noop(
|
||||
)
|
||||
|
||||
|
||||
class FirehoseLayout(FirehoseLayoutBase):
|
||||
class FirehoseLayout(Widget):
|
||||
PARAM_KEY = "ApiCache_FirehoseStats"
|
||||
GREEN = rl.Color(46, 204, 113, 255)
|
||||
RED = rl.Color(231, 76, 60, 255)
|
||||
GRAY = rl.Color(68, 68, 68, 255)
|
||||
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
|
||||
UPDATE_INTERVAL = 30 # seconds
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._scroll_panel = GuiScrollPanel()
|
||||
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:
|
||||
return 0
|
||||
try:
|
||||
return int(stats.get("firehose", 0))
|
||||
except Exception:
|
||||
cloudlog.exception(f"Failed to decode firehose stats: {stats}")
|
||||
return 0
|
||||
|
||||
def __del__(self):
|
||||
self.running = False
|
||||
if self.update_thread and self.update_thread.is_alive():
|
||||
self.update_thread.join(timeout=1.0)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate content dimensions
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
|
||||
|
||||
# Handle scrolling and render with clipping
|
||||
scroll_offset = self._scroll_panel.update(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._content_height = self._render_content(rect, scroll_offset)
|
||||
rl.end_scissor_mode()
|
||||
@@ -66,9 +107,9 @@ class FirehoseLayout(FirehoseLayoutBase):
|
||||
y += 20 + 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self._segment_count > 0:
|
||||
if self.segment_count > 0:
|
||||
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)
|
||||
"{} 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 + 20
|
||||
|
||||
@@ -80,7 +121,7 @@ class FirehoseLayout(FirehoseLayoutBase):
|
||||
y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
|
||||
# bottom margin + remove effect of scroll offset
|
||||
return int(round(y - self._scroll_panel.offset + 40))
|
||||
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)
|
||||
@@ -88,3 +129,32 @@ class FirehoseLayout(FirehoseLayoutBase):
|
||||
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 tr("ACTIVE"), self.GREEN
|
||||
else:
|
||||
return tr("INACTIVE: connect to an unmetered network"), self.RED
|
||||
|
||||
def _fetch_firehose_stats(self):
|
||||
try:
|
||||
dongle_id = self.params.get("DongleId")
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
identity_token = get_token(dongle_id)
|
||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self.segment_count = data.get("firehose", 0)
|
||||
self.params.put(self.PARAM_KEY, data)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch firehose stats: {e}")
|
||||
|
||||
def _update_loop(self):
|
||||
while self.running:
|
||||
if not ui_state.started:
|
||||
self._fetch_firehose_stats()
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
|
||||
@@ -11,19 +11,9 @@ from openpilot.system.ui.widgets.list_view import button_item, text_item, ListIt
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
|
||||
|
||||
# TODO: remove this. updater fails to respond on startup if time is not correct
|
||||
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
|
||||
|
||||
# Mapping updater internal states to translated display strings
|
||||
STATE_TO_DISPLAY_TEXT = {
|
||||
"checking...": tr("checking..."),
|
||||
"downloading...": tr("downloading..."),
|
||||
"finalizing update...": tr("finalizing update..."),
|
||||
}
|
||||
|
||||
|
||||
def time_ago(date: datetime.datetime | None) -> str:
|
||||
if not date:
|
||||
@@ -110,9 +100,7 @@ class SoftwareLayout(Widget):
|
||||
# Updater responded
|
||||
self._waiting_for_updater = False
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
# Use the mapping, with a fallback to the original state string
|
||||
display_text = STATE_TO_DISPLAY_TEXT.get(updater_state, updater_state)
|
||||
self._download_btn.action_item.set_value(display_text)
|
||||
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"))
|
||||
|
||||
@@ -11,27 +11,26 @@ from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp as multiple_button_item
|
||||
|
||||
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
"OpenpilotEnabledToggle": tr_noop(
|
||||
"Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"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": tr_noop("When enabled, pressing the accelerator pedal will disengage sunnypilot."),
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
|
||||
"LongitudinalPersonality": tr_noop(
|
||||
"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 " +
|
||||
"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": 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": tr_noop("Enable driver monitoring even when sunnypilot is not engaged."),
|
||||
"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."),
|
||||
@@ -47,7 +46,7 @@ class TogglesLayout(Widget):
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
lambda: tr("Enable sunnypilot"),
|
||||
lambda: tr("Enable openpilot"),
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
"chffr_wheel.png",
|
||||
True,
|
||||
@@ -100,7 +99,7 @@ class TogglesLayout(Widget):
|
||||
lambda: tr("Driving Personality"),
|
||||
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
|
||||
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
|
||||
button_width=300,
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
|
||||
icon="speed_limit.png"
|
||||
@@ -126,7 +125,7 @@ class TogglesLayout(Widget):
|
||||
# Make description callable for live translation
|
||||
additional_desc = ""
|
||||
if needs_restart and not locked:
|
||||
additional_desc = tr("Changing this setting will restart sunnypilot if the car is powered on.")
|
||||
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
|
||||
@@ -159,10 +158,10 @@ class TogglesLayout(Widget):
|
||||
ui_state.update_params()
|
||||
|
||||
e2e_description = tr(
|
||||
"sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
|
||||
"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:<br>" +
|
||||
"<h4>End-to-End Longitudinal Control</h4><br>" +
|
||||
"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. " +
|
||||
"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.<br>" +
|
||||
"<h4>New Driving Visualization</h4><br>" +
|
||||
@@ -184,13 +183,13 @@ class TogglesLayout(Widget):
|
||||
|
||||
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("sunnypilot longitudinal control may come in a future update.")
|
||||
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 sunnypilot longitudinal control can be tested, along with " +
|
||||
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 sunnypilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||
long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
|
||||
|
||||
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
|
||||
else:
|
||||
|
||||
@@ -67,10 +67,8 @@ class PrimeState:
|
||||
cloudlog.info(f"Prime type updated to {prime_type}")
|
||||
|
||||
def _worker_thread(self) -> None:
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
while self._running:
|
||||
if not ui_state.started and device._awake:
|
||||
self._fetch_prime_status()
|
||||
self._fetch_prime_status()
|
||||
|
||||
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
|
||||
if not self._running:
|
||||
|
||||
@@ -3,16 +3,18 @@ import time
|
||||
from cereal import log
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel
|
||||
from openpilot.system.ui.widgets.label import gui_label, MiciLabel
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.text import wrap_text
|
||||
from openpilot.system.version import training_version, RELEASE_BRANCHES
|
||||
from openpilot.system.version import training_version
|
||||
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
HOME_PADDING = 8
|
||||
|
||||
RELEASE_BRANCH = "release3"
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
NETWORK_TYPES = {
|
||||
@@ -43,7 +45,7 @@ class DeviceStatus(Widget):
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "sunnypilot"
|
||||
brand = "openpilot"
|
||||
description = ui_state.params.get("UpdaterCurrentDescription")
|
||||
return f"{brand} {description}" if description else brand
|
||||
|
||||
@@ -109,11 +111,11 @@ class MiciHomeLayout(Widget):
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35)
|
||||
|
||||
self._openpilot_label = MiciLabel("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
self._openpilot_label = MiciLabel("openpilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
|
||||
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
self._branch_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN, elide_right=False, scroll=True)
|
||||
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
|
||||
def show_event(self):
|
||||
@@ -185,22 +187,27 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
if self._version_text is not None:
|
||||
# release branch
|
||||
release_branch = self._version_text[1] in RELEASE_BRANCHES
|
||||
version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44)
|
||||
self._version_label.set_text(self._version_text[0])
|
||||
self._version_label.set_position(version_pos.x, version_pos.y)
|
||||
self._version_label.render()
|
||||
if self._version_text[0] == RELEASE_BRANCH:
|
||||
version_pos = rl.Vector2(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16)
|
||||
self._large_version_label.set_text(self._version_text[0])
|
||||
self._large_version_label.set_position(version_pos.x, version_pos.y)
|
||||
self._large_version_label.render()
|
||||
|
||||
self._date_label.set_text(" " + self._version_text[3])
|
||||
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
|
||||
self._date_label.render()
|
||||
else:
|
||||
version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44)
|
||||
self._version_label.set_text(self._version_text[0])
|
||||
self._version_label.set_position(version_pos.x, version_pos.y)
|
||||
self._version_label.render()
|
||||
|
||||
self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
|
||||
self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1]))
|
||||
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
|
||||
self._branch_label.render()
|
||||
self._date_label.set_text(" " + self._version_text[3])
|
||||
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
|
||||
self._date_label.render()
|
||||
|
||||
self._branch_label.set_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
|
||||
self._branch_label.set_text(" " + self._version_text[1])
|
||||
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
|
||||
self._branch_label.render()
|
||||
|
||||
if not release_branch:
|
||||
# 2nd line
|
||||
self._version_commit_label.set_text(self._version_text[2])
|
||||
self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7)
|
||||
|
||||
@@ -5,7 +5,6 @@ from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
@@ -221,7 +220,6 @@ class MiciOffroadAlerts(Widget):
|
||||
update_alert_data = AlertData(key="UpdateAvailable", text="", severity=-1)
|
||||
self.sorted_alerts.append(update_alert_data)
|
||||
update_alert_item = AlertItem(update_alert_data)
|
||||
update_alert_item.set_click_callback(lambda: HARDWARE.reboot())
|
||||
self.alert_items.append(update_alert_item)
|
||||
self._scroller.add_widget(update_alert_item)
|
||||
|
||||
@@ -246,18 +244,18 @@ class MiciOffroadAlerts(Widget):
|
||||
|
||||
if update_alert_data:
|
||||
if update_available:
|
||||
version_string = ""
|
||||
# Default text
|
||||
update_alert_data.text = "update available. go to comma.ai/blog to read the release notes."
|
||||
|
||||
# Get new version description and parse version and date
|
||||
new_desc = self.params.get("UpdaterNewDescription") or ""
|
||||
if new_desc:
|
||||
# format: "version / branch / commit / date"
|
||||
# Parse description (format: "version / branch / commit / date")
|
||||
parts = new_desc.split(" / ")
|
||||
if len(parts) > 3:
|
||||
version, date = parts[0], parts[3]
|
||||
version_string = f"\nsunnypilot {version}, {date}\n"
|
||||
update_alert_data.text = f"update available\n openpilot {version}, {date}. go to comma.ai/blog to read the release notes."
|
||||
|
||||
update_alert_data.text = f"Update available {version_string}. Click to update. Read the release notes at blog.comma.ai."
|
||||
update_alert_data.visible = True
|
||||
active_count += 1
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
import weakref
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -93,10 +92,11 @@ class TrainingGuideDMTutorial(Widget):
|
||||
super().__init__()
|
||||
self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._original_continue_callback = continue_callback
|
||||
|
||||
# Wrap the continue callback to restore settings
|
||||
def wrapped_continue_callback():
|
||||
device.set_offroad_brightness(None)
|
||||
device.reset_interactive_timeout()
|
||||
self._restore_settings()
|
||||
continue_callback()
|
||||
|
||||
self._dialog = DriverCameraSetupDialog(wrapped_continue_callback)
|
||||
@@ -114,6 +114,10 @@ class TrainingGuideDMTutorial(Widget):
|
||||
device.set_offroad_brightness(100)
|
||||
device.reset_interactive_timeout(300) # 5 minutes
|
||||
|
||||
def _restore_settings(self):
|
||||
device.set_offroad_brightness(None)
|
||||
device.reset_interactive_timeout()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if device.awake:
|
||||
@@ -146,7 +150,7 @@ class TrainingGuideRecordFront(SetupTermsPage):
|
||||
super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes")
|
||||
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42,
|
||||
self._dm_label = UnifiedLabel("Do you want to upload driver camera data to improve driver monitoring?", 42,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
def show_event(self):
|
||||
@@ -178,7 +182,7 @@ class TrainingGuideAttentionNotice(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
super().__init__(continue_callback, continue_text="continue")
|
||||
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
|
||||
self._warning_label = UnifiedLabel("1. sunnypilot is a driver assistance system.\n\n" +
|
||||
self._warning_label = UnifiedLabel("1. openpilot is a driver assistance system.\n\n" +
|
||||
"2. You must pay attention at all times.\n\n" +
|
||||
"3. You must be ready to take over at any time.\n\n" +
|
||||
"4. You are fully responsible for driving the car.", 42,
|
||||
@@ -210,17 +214,11 @@ class TrainingGuide(Widget):
|
||||
self._completed_callback = completed_callback
|
||||
self._step = 0
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
|
||||
def on_continue():
|
||||
if obj := self_ref():
|
||||
obj._advance_step()
|
||||
|
||||
self._steps = [
|
||||
TrainingGuideAttentionNotice(continue_callback=on_continue),
|
||||
TrainingGuidePreDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideRecordFront(continue_callback=on_continue),
|
||||
TrainingGuideAttentionNotice(continue_callback=self._advance_step),
|
||||
TrainingGuidePreDMTutorial(continue_callback=self._advance_step),
|
||||
TrainingGuideDMTutorial(continue_callback=self._advance_step),
|
||||
TrainingGuideRecordFront(continue_callback=self._advance_step),
|
||||
]
|
||||
|
||||
def _advance_step(self):
|
||||
@@ -241,12 +239,12 @@ class TrainingGuide(Widget):
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._uninstall_slider = SmallSlider("uninstall sunnypilot", self._on_uninstall)
|
||||
self._uninstall_slider = SmallSlider("uninstall openpilot", self._on_uninstall)
|
||||
|
||||
self._back_button = SmallButton("back")
|
||||
self._back_button.set_click_callback(back_callback)
|
||||
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use sunnypilot",
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use openpilot",
|
||||
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
|
||||
|
||||
def _on_uninstall(self):
|
||||
@@ -284,7 +282,7 @@ class TermsPage(SetupTermsPage):
|
||||
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
|
||||
self._title_header = TermsHeader("terms & conditions", info_txt)
|
||||
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use sunnypilot. " +
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use openpilot. " +
|
||||
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class MiciFccModal(NavWidget):
|
||||
content_height += self._fcc_logo.height + 20
|
||||
|
||||
scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
||||
scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height))
|
||||
scroll_offset = self._scroll_panel.update(rect, scroll_content_rect.height)
|
||||
|
||||
fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset)
|
||||
|
||||
@@ -156,7 +156,7 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64)
|
||||
self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64)
|
||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
super().__init__("update sunnypilot", "", self._txt_update_icon)
|
||||
super().__init__("update openpilot", "", self._txt_update_icon)
|
||||
|
||||
self._waiting_for_updater_t: float | None = None
|
||||
self._hide_value_t: float | None = None
|
||||
@@ -193,7 +193,7 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
if value:
|
||||
self.set_text("")
|
||||
else:
|
||||
self.set_text("update sunnypilot")
|
||||
self.set_text("update openpilot")
|
||||
|
||||
def _update_state(self):
|
||||
if ui_state.started:
|
||||
@@ -294,7 +294,7 @@ class DeviceLayoutMici(NavWidget):
|
||||
reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png")
|
||||
reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
|
||||
|
||||
uninstall_openpilot_btn = BigButton("uninstall sunnypilot", "", "icons_mici/settings/device/uninstall.png")
|
||||
uninstall_openpilot_btn = BigButton("uninstall openpilot", "", "icons_mici/settings/device/uninstall.png")
|
||||
uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall"))
|
||||
|
||||
reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False)
|
||||
|
||||
@@ -6,17 +6,18 @@ from openpilot.common.api import api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.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, FONT_SCALE
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
|
||||
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
"sunnypilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
"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."
|
||||
)
|
||||
@@ -33,7 +34,9 @@ FAQ_ITEMS = [
|
||||
]
|
||||
|
||||
|
||||
class FirehoseLayoutBase(Widget):
|
||||
class FirehoseLayoutMici(NavWidget):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
|
||||
PARAM_KEY = "ApiCache_FirehoseStats"
|
||||
GREEN = rl.Color(46, 204, 113, 255)
|
||||
RED = rl.Color(231, 76, 60, 255)
|
||||
@@ -41,10 +44,12 @@ class FirehoseLayoutBase(Widget):
|
||||
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
|
||||
UPDATE_INTERVAL = 30 # seconds
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, back_callback):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._segment_count = self._get_segment_count()
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
self.params = Params()
|
||||
self.segment_count = self._get_segment_count()
|
||||
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._content_height = 0
|
||||
@@ -66,7 +71,7 @@ class FirehoseLayoutBase(Widget):
|
||||
self._scroll_panel.set_offset(0)
|
||||
|
||||
def _get_segment_count(self) -> int:
|
||||
stats = self._params.get(self.PARAM_KEY)
|
||||
stats = self.params.get(self.PARAM_KEY)
|
||||
if not stats:
|
||||
return 0
|
||||
try:
|
||||
@@ -78,7 +83,7 @@ class FirehoseLayoutBase(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# compute total content height for scrolling
|
||||
content_height = self._measure_content_height(rect)
|
||||
scroll_offset = round(self._scroll_panel.update(rect, content_height))
|
||||
scroll_offset = self._scroll_panel.update(rect, content_height)
|
||||
|
||||
# start drawing with offset
|
||||
x = int(rect.x + 40)
|
||||
@@ -106,9 +111,9 @@ class FirehoseLayoutBase(Widget):
|
||||
y += 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self._segment_count > 0:
|
||||
if self.segment_count > 0:
|
||||
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)
|
||||
"{} 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), 42, rl.WHITE)
|
||||
y += 20
|
||||
|
||||
@@ -160,9 +165,9 @@ class FirehoseLayoutBase(Widget):
|
||||
y += int(len(status_lines) * 48 * FONT_SCALE) + 20
|
||||
|
||||
# Contribution count
|
||||
if self._segment_count > 0:
|
||||
if self.segment_count > 0:
|
||||
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)
|
||||
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
|
||||
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 42, w)
|
||||
y += int(len(contrib_lines) * 42 * FONT_SCALE) + 20
|
||||
|
||||
@@ -199,28 +204,20 @@ class FirehoseLayoutBase(Widget):
|
||||
|
||||
def _fetch_firehose_stats(self):
|
||||
try:
|
||||
dongle_id = self._params.get("DongleId")
|
||||
dongle_id = self.params.get("DongleId")
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
identity_token = get_token(dongle_id)
|
||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self._segment_count = data.get("firehose", 0)
|
||||
self._params.put(self.PARAM_KEY, data)
|
||||
self.segment_count = data.get("firehose", 0)
|
||||
self.params.put(self.PARAM_KEY, data)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch firehose stats: {e}")
|
||||
|
||||
def _update_loop(self):
|
||||
while self._running:
|
||||
if not ui_state.started and device._awake:
|
||||
if not ui_state.started:
|
||||
self._fetch_firehose_stats()
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
|
||||
|
||||
class FirehoseLayout(FirehoseLayoutBase, NavWidget):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
|
||||
def __init__(self, back_callback):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import math
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, MeteredType
|
||||
|
||||
|
||||
def normalize_ssid(ssid: str) -> str:
|
||||
return ssid.replace("’", "'") # for iPhone hotspots
|
||||
|
||||
|
||||
class NetworkPanelType(IntEnum):
|
||||
NONE = 0
|
||||
WIFI = 1
|
||||
|
||||
|
||||
class LoadingAnimation(Widget):
|
||||
def _render(self, _):
|
||||
cx = int(self._rect.x + 70)
|
||||
@@ -83,13 +91,11 @@ class WifiIcon(Widget):
|
||||
|
||||
class WifiItem(BigDialogOptionButton):
|
||||
LEFT_MARGIN = 20
|
||||
HEIGHT = 54
|
||||
SELECTED_HEIGHT = 74
|
||||
|
||||
def __init__(self, network: Network):
|
||||
super().__init__(network.ssid)
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT))
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, 64))
|
||||
|
||||
self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96)
|
||||
|
||||
@@ -97,10 +103,6 @@ class WifiItem(BigDialogOptionButton):
|
||||
self._wifi_icon = WifiIcon()
|
||||
self._wifi_icon.set_current_network(network)
|
||||
|
||||
def set_selected(self, selected: bool):
|
||||
super().set_selected(selected)
|
||||
self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT
|
||||
|
||||
def set_current_network(self, network: Network):
|
||||
self._network = network
|
||||
self._wifi_icon.set_current_network(network)
|
||||
@@ -115,7 +117,7 @@ class WifiItem(BigDialogOptionButton):
|
||||
self._wifi_icon.render(rl.Rectangle(
|
||||
self._rect.x + self.LEFT_MARGIN,
|
||||
self._rect.y,
|
||||
self.SELECTED_HEIGHT,
|
||||
self._rect.height,
|
||||
self._rect.height
|
||||
))
|
||||
|
||||
@@ -124,7 +126,7 @@ class WifiItem(BigDialogOptionButton):
|
||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
self._label.set_font_weight(FontWeight.DISPLAY)
|
||||
else:
|
||||
self._label.set_font_size(54)
|
||||
self._label.set_font_size(70)
|
||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
|
||||
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||
|
||||
@@ -213,7 +215,7 @@ class NetworkInfoPage(NavWidget):
|
||||
self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None)
|
||||
|
||||
self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True)
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
@@ -223,10 +225,6 @@ class NetworkInfoPage(NavWidget):
|
||||
self._network: Network | None = None
|
||||
self._connecting: Callable[[], str | None] | None = None
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._title.reset_scroll()
|
||||
|
||||
def update_networks(self, networks: dict[str, Network]):
|
||||
# update current network from latest scan results
|
||||
for ssid, network in networks.items():
|
||||
@@ -394,7 +392,7 @@ class WifiUIMici(BigMultiOptionDialog):
|
||||
self._network_info_page.set_current_network(_network)
|
||||
self._should_open_network_info_page = True
|
||||
|
||||
network_button.set_click_callback(lambda _net=network, _button=network_button: _button._selected and show_network_info_page(_net))
|
||||
network_button.set_click_callback(lambda _net=network,_button=network_button: _button._selected and show_network_info_page(_net))
|
||||
|
||||
self.add_button(network_button)
|
||||
|
||||
@@ -445,3 +443,116 @@ class WifiUIMici(BigMultiOptionDialog):
|
||||
|
||||
if not self._networks:
|
||||
self._loading_animation.render(self._rect)
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = NetworkPanelType.WIFI
|
||||
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
_tethering_icon = "icons_mici/settings/network/tethering.png"
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture(_tethering_icon, 64, 53)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton("IP Address", "Not connected")
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
wifi_button = BigButton("wi-fi")
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._current_panel = NetworkPanelType.NONE
|
||||
self._wifi_ui.show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_ui.hide_event()
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
|
||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||
self._current_panel = panel_type
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._wifi_manager.process_callbacks()
|
||||
|
||||
if self._current_panel == NetworkPanelType.WIFI:
|
||||
self._wifi_ui.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
@@ -1,182 +0,0 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
|
||||
|
||||
class NetworkPanelType(IntEnum):
|
||||
NONE = 0
|
||||
WIFI = 1
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = NetworkPanelType.WIFI
|
||||
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
_tethering_icon = "icons_mici/settings/network/tethering.png"
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture(_tethering_icon, 64, 53)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton("IP Address", "Not connected")
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
wifi_button = BigButton("wi-fi")
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
# /* Advanced settings
|
||||
self._roaming_btn,
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set initial config
|
||||
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
|
||||
metered = ui_state.params.get_bool("GsmMetered")
|
||||
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# 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 show_event(self):
|
||||
super().show_event()
|
||||
self._current_panel = NetworkPanelType.NONE
|
||||
self._wifi_ui.show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_ui.hide_event()
|
||||
|
||||
def _toggle_roaming(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
def _edit_apn(self):
|
||||
def update_apn(apn: str):
|
||||
apn = apn.strip()
|
||||
if apn == "":
|
||||
ui_state.params.remove("GsmApn")
|
||||
else:
|
||||
ui_state.params.put("GsmApn", apn)
|
||||
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
current_apn = ui_state.params.get("GsmApn") or ""
|
||||
dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _toggle_cellular_metered(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
|
||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||
self._current_panel = panel_type
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._wifi_manager.process_callbacks()
|
||||
|
||||
if self._current_panel == NetworkPanelType.WIFI:
|
||||
self._wifi_ui.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
@@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutMici
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
|
||||
@@ -67,7 +67,7 @@ class SettingsLayout(NavWidget):
|
||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
@@ -24,7 +24,7 @@ class TogglesLayoutMici(NavWidget):
|
||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
||||
record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback)
|
||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._personality_toggle,
|
||||
|
||||
@@ -65,7 +65,7 @@ class Alert:
|
||||
|
||||
# Pre-defined alert instances
|
||||
ALERT_STARTUP_PENDING = Alert(
|
||||
text1="sunnypilot Unavailable",
|
||||
text1="openpilot Unavailable",
|
||||
text2="Waiting to start",
|
||||
size=AlertSize.mid,
|
||||
status=AlertStatus.normal,
|
||||
@@ -89,6 +89,10 @@ ALERT_CRITICAL_REBOOT = Alert(
|
||||
class AlertRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.font_regular: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
self.font_roman: rl.Font = gui_app.font(FontWeight.ROMAN)
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self.font_display: rl.Font = gui_app.font(FontWeight.DISPLAY)
|
||||
|
||||
self._alert_text1_label = UnifiedLabel(text="", font_size=ALERT_FONT_BIG, font_weight=FontWeight.DISPLAY, line_height=0.86,
|
||||
letter_spacing=-0.02)
|
||||
@@ -200,11 +204,11 @@ class AlertRenderer(Widget):
|
||||
text_x = self._rect.x + ALERT_MARGIN
|
||||
text_width = self._rect.width - ALERT_MARGIN
|
||||
if icon_side == 'left':
|
||||
text_x = self._rect.x + self._txt_turn_signal_right.width
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width
|
||||
text_x = self._rect.x + self._txt_turn_signal_right.width + 20 * 2
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2
|
||||
elif icon_side == 'right':
|
||||
text_x = self._rect.x + ALERT_MARGIN
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2
|
||||
|
||||
text_rect = rl.Rectangle(
|
||||
text_x,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from cereal import messaging, car, log
|
||||
from cereal import car, log
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
|
||||
@@ -154,16 +153,13 @@ class AugmentedRoadView(CameraView):
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._driver_state_renderer = DriverStateRenderer()
|
||||
self._confidence_ball = ConfidenceBall()
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 54, FontWeight.DISPLAY,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||
|
||||
# debug
|
||||
self._pm = messaging.PubMaster(['uiDebug'])
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
"""Check if currently swiping left (for scroller to disable)."""
|
||||
return self._bookmark_icon.is_swiping_left()
|
||||
@@ -175,7 +171,7 @@ class AugmentedRoadView(CameraView):
|
||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||
self._offroad_label.set_text("system booting")
|
||||
else:
|
||||
self._offroad_label.set_text("start the car to\nuse sunnypilot")
|
||||
self._offroad_label.set_text("start the car to\nuse openpilot")
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
# Don't trigger click callback if bookmark was triggered
|
||||
@@ -183,7 +179,6 @@ class AugmentedRoadView(CameraView):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _render(self, _):
|
||||
start_draw = time.monotonic()
|
||||
self._switch_stream_if_needed(ui_state.sm)
|
||||
|
||||
# Update calibration before rendering
|
||||
@@ -249,11 +244,6 @@ class AugmentedRoadView(CameraView):
|
||||
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
|
||||
self._offroad_label.render(self._content_rect)
|
||||
|
||||
# publish uiDebug
|
||||
msg = messaging.new_message('uiDebug')
|
||||
msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000
|
||||
self._pm.send('uiDebug', msg)
|
||||
|
||||
def _switch_stream_if_needed(self, sm):
|
||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||
v_ego = sm['carState'].vEgo
|
||||
|
||||
@@ -107,6 +107,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)
|
||||
@@ -196,10 +197,7 @@ class CameraView(Widget):
|
||||
# Clean up shader
|
||||
if self.shader and self.shader.id:
|
||||
rl.unload_shader(self.shader)
|
||||
self.shader.id = 0
|
||||
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
self.client = None
|
||||
|
||||
def __del__(self):
|
||||
@@ -236,9 +234,6 @@ 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)
|
||||
|
||||
@@ -15,27 +15,20 @@ EventName = log.OnroadEvent.EventName
|
||||
EVENT_TO_INT = EventName.schema.enumerants
|
||||
|
||||
|
||||
class DriverCameraView(CameraView):
|
||||
def _calc_frame_matrix(self, rect: rl.Rectangle):
|
||||
base = super()._calc_frame_matrix(rect)
|
||||
driver_view_ratio = 1.5
|
||||
base[0, 0] *= driver_view_ratio
|
||||
base[1, 1] *= driver_view_ratio
|
||||
return base
|
||||
|
||||
|
||||
class DriverCameraDialog(NavWidget):
|
||||
def __init__(self, no_escape=False):
|
||||
super().__init__()
|
||||
self._camera_view = DriverCameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self._camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self._original_calc_frame_matrix = self._camera_view._calc_frame_matrix
|
||||
self._camera_view._calc_frame_matrix = self._calc_driver_frame_matrix
|
||||
self.driver_state_renderer = DriverStateRenderer(lines=True)
|
||||
self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200))
|
||||
self.driver_state_renderer.load_icons()
|
||||
self._pm: messaging.PubMaster | None = None
|
||||
self._pm = messaging.PubMaster(['selfdriveState'])
|
||||
if not no_escape:
|
||||
# TODO: this can grow unbounded, should be given some thought
|
||||
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld)
|
||||
self.set_back_callback(self._dismiss)
|
||||
self.set_back_enabled(not no_escape)
|
||||
|
||||
# Load eye icons
|
||||
@@ -47,24 +40,26 @@ class DriverCameraDialog(NavWidget):
|
||||
|
||||
self._load_eye_textures()
|
||||
|
||||
def stop_dmonitoringmodeld(self):
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
self._publish_alert_sound(None)
|
||||
device.reset_interactive_timeout(300)
|
||||
ui_state.params.remove("DriverTooDistracted")
|
||||
self._pm = messaging.PubMaster(['selfdriveState'])
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
device.reset_interactive_timeout()
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
ui_state.params.remove("DriverTooDistracted")
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
def _dismiss(self):
|
||||
self.stop_dmonitoringmodeld()
|
||||
|
||||
def close(self):
|
||||
if self._camera_view:
|
||||
@@ -108,9 +103,6 @@ class DriverCameraDialog(NavWidget):
|
||||
|
||||
def _publish_alert_sound(self, dm_state):
|
||||
"""Publish selfdriveState with only alertSound field set"""
|
||||
if self._pm is None:
|
||||
return
|
||||
|
||||
msg = messaging.new_message('selfdriveState')
|
||||
if dm_state is not None and len(dm_state.events):
|
||||
event_name = EVENT_TO_INT[dm_state.events[0].name]
|
||||
@@ -229,6 +221,13 @@ class DriverCameraDialog(NavWidget):
|
||||
glasses_prob = driver_data.sunglassesProb
|
||||
rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob)))
|
||||
|
||||
def _calc_driver_frame_matrix(self, rect: rl.Rectangle):
|
||||
base = self._original_calc_frame_matrix(rect)
|
||||
driver_view_ratio = 1.5
|
||||
base[0, 0] *= driver_view_ratio
|
||||
base[1, 1] *= driver_view_ratio
|
||||
return base
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("Driver Camera View (mici)")
|
||||
|
||||
@@ -3,7 +3,6 @@ from collections.abc import Callable
|
||||
import numpy as np
|
||||
import math
|
||||
from cereal import log
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -218,10 +217,7 @@ class DriverStateRenderer(Widget):
|
||||
rotation = math.degrees(math.atan2(pitch, yaw))
|
||||
angle_diff = rotation - self._rotation_filter.x
|
||||
angle_diff = ((angle_diff + 180) % 360) - 180
|
||||
if PC and self._confirm_mode:
|
||||
self._rotation_filter.x += 2
|
||||
else:
|
||||
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
||||
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
||||
|
||||
if not self.should_draw:
|
||||
self._fade_filter.update(0.0)
|
||||
|
||||
@@ -30,8 +30,20 @@ class FontSizes:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Colors:
|
||||
WHITE = rl.WHITE
|
||||
WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200)
|
||||
white: rl.Color = rl.WHITE
|
||||
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
|
||||
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
|
||||
engaged: rl.Color = rl.Color(128, 216, 166, 255)
|
||||
disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153)
|
||||
override_bg: rl.Color = rl.Color(145, 155, 149, 204)
|
||||
engaged_bg: rl.Color = rl.Color(128, 216, 166, 204)
|
||||
grey: rl.Color = rl.Color(166, 166, 166, 255)
|
||||
dark_grey: rl.Color = rl.Color(114, 114, 114, 255)
|
||||
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
|
||||
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
|
||||
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
|
||||
header_gradient_end: rl.Color = rl.BLANK
|
||||
|
||||
|
||||
FONT_SIZES = FontSizes()
|
||||
@@ -224,18 +236,16 @@ class HudRenderer(Widget):
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and
|
||||
self._can_draw_top_icons and self._engaged)
|
||||
if alpha < 1e-2:
|
||||
return
|
||||
|
||||
x = rect.x
|
||||
y = rect.y
|
||||
|
||||
alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and
|
||||
self._can_draw_top_icons and self._engaged)
|
||||
|
||||
# draw drop shadow
|
||||
circle_radius = 162 // 2
|
||||
rl.draw_circle_gradient(int(x + circle_radius), int(y + circle_radius), circle_radius,
|
||||
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.BLANK)
|
||||
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.Color(0, 0, 0, 0))
|
||||
|
||||
set_speed_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
|
||||
max_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
|
||||
@@ -269,9 +279,9 @@ class HudRenderer(Widget):
|
||||
speed_text = str(round(self.speed))
|
||||
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
|
||||
unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
@@ -130,9 +130,6 @@ def arc_bar_pts(cx: float, cy: float,
|
||||
|
||||
pts = np.vstack((outer, cap_end, inner, cap_start, outer[:1])).astype(np.float32)
|
||||
|
||||
# Rotate to start from middle of cap for proper triangulation
|
||||
pts = np.roll(pts, cap_segs, axis=0)
|
||||
|
||||
if DEBUG:
|
||||
n = len(pts)
|
||||
idx = int(time.monotonic() * 12) % max(1, n) # speed: 12 pts/sec
|
||||
|
||||
@@ -282,8 +282,7 @@ class BigDialogOptionButton(Widget):
|
||||
self._selected = False
|
||||
|
||||
self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
|
||||
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
scroll=True)
|
||||
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
|
||||
def set_selected(self, selected: bool):
|
||||
self._selected = selected
|
||||
|
||||
@@ -48,7 +48,7 @@ class Alert:
|
||||
|
||||
# Pre-defined alert instances
|
||||
ALERT_STARTUP_PENDING = Alert(
|
||||
text1=tr("sunnypilot Unavailable"),
|
||||
text1=tr("openpilot Unavailable"),
|
||||
text2=tr("Waiting to start"),
|
||||
size=AlertSize.mid,
|
||||
status=AlertStatus.normal,
|
||||
|
||||
@@ -68,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)
|
||||
@@ -336,12 +337,12 @@ class CameraView(Widget):
|
||||
self._initialize_textures()
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
if not TICI:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
|
||||
self._clear_textures()
|
||||
if not TICI:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
|
||||
|
||||
def _clear_textures(self):
|
||||
if self.texture_y and self.texture_y.id:
|
||||
|
||||
@@ -35,20 +35,20 @@ class FontSizes:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Colors:
|
||||
WHITE = rl.WHITE
|
||||
DISENGAGED = rl.Color(145, 155, 149, 255)
|
||||
OVERRIDE = rl.Color(145, 155, 149, 255) # Added
|
||||
ENGAGED = rl.Color(128, 216, 166, 255)
|
||||
DISENGAGED_BG = rl.Color(0, 0, 0, 153)
|
||||
OVERRIDE_BG = rl.Color(145, 155, 149, 204)
|
||||
ENGAGED_BG = rl.Color(128, 216, 166, 204)
|
||||
GREY = rl.Color(166, 166, 166, 255)
|
||||
DARK_GREY = rl.Color(114, 114, 114, 255)
|
||||
BLACK_TRANSLUCENT = rl.Color(0, 0, 0, 166)
|
||||
WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200)
|
||||
BORDER_TRANSLUCENT = rl.Color(255, 255, 255, 75)
|
||||
HEADER_GRADIENT_START = rl.Color(0, 0, 0, 114)
|
||||
HEADER_GRADIENT_END = rl.BLANK
|
||||
white: rl.Color = rl.WHITE
|
||||
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
|
||||
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
|
||||
engaged: rl.Color = rl.Color(128, 216, 166, 255)
|
||||
disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153)
|
||||
override_bg: rl.Color = rl.Color(145, 155, 149, 204)
|
||||
engaged_bg: rl.Color = rl.Color(128, 216, 166, 204)
|
||||
grey: rl.Color = rl.Color(166, 166, 166, 255)
|
||||
dark_grey: rl.Color = rl.Color(114, 114, 114, 255)
|
||||
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
|
||||
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
|
||||
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
|
||||
header_gradient_end: rl.Color = rl.BLANK
|
||||
|
||||
|
||||
UI_CONFIG = UIConfig()
|
||||
@@ -108,8 +108,8 @@ class HudRenderer(Widget):
|
||||
int(rect.y),
|
||||
int(rect.width),
|
||||
UI_CONFIG.header_height,
|
||||
COLORS.HEADER_GRADIENT_START,
|
||||
COLORS.HEADER_GRADIENT_END,
|
||||
COLORS.header_gradient_start,
|
||||
COLORS.header_gradient_end,
|
||||
)
|
||||
|
||||
if self.is_cruise_available:
|
||||
@@ -131,19 +131,19 @@ 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.35, 10, COLORS.BLACK_TRANSLUCENT)
|
||||
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 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
|
||||
max_color = COLORS.grey
|
||||
set_speed_color = COLORS.dark_grey
|
||||
if self.is_cruise_set:
|
||||
set_speed_color = COLORS.WHITE
|
||||
set_speed_color = COLORS.white
|
||||
if ui_state.status == UIStatus.ENGAGED:
|
||||
max_color = COLORS.ENGAGED
|
||||
max_color = COLORS.engaged
|
||||
elif ui_state.status == UIStatus.DISENGAGED:
|
||||
max_color = COLORS.DISENGAGED
|
||||
max_color = COLORS.disengaged
|
||||
elif ui_state.status == UIStatus.OVERRIDE:
|
||||
max_color = COLORS.OVERRIDE
|
||||
max_color = COLORS.override
|
||||
|
||||
max_text = tr("MAX")
|
||||
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
|
||||
@@ -172,9 +172,9 @@ class HudRenderer(Widget):
|
||||
speed_text = str(round(self.speed))
|
||||
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
|
||||
unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
158
selfdrive/ui/qt/offroad/offroad_home.cc
Normal file
158
selfdrive/ui/qt/offroad/offroad_home.cc
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/offroad_home.h"
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/widgets/prime.h"
|
||||
|
||||
// OffroadHome: the offroad home page
|
||||
|
||||
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(40, 40, 40, 40);
|
||||
|
||||
// top header
|
||||
header_layout = new QHBoxLayout();
|
||||
header_layout->setContentsMargins(0, 0, 0, 0);
|
||||
header_layout->setSpacing(16);
|
||||
|
||||
update_notif = new QPushButton(tr("UPDATE"));
|
||||
update_notif->setVisible(false);
|
||||
update_notif->setStyleSheet("background-color: #364DEF;");
|
||||
QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); });
|
||||
header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
||||
|
||||
alert_notif = new QPushButton();
|
||||
alert_notif->setVisible(false);
|
||||
alert_notif->setStyleSheet("background-color: #E22C2C;");
|
||||
QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); });
|
||||
header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
||||
|
||||
version = new ElidedLabel();
|
||||
header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight);
|
||||
|
||||
main_layout->addLayout(header_layout);
|
||||
|
||||
// main content
|
||||
main_layout->addSpacing(25);
|
||||
center_layout = new QStackedLayout();
|
||||
|
||||
QWidget *home_widget = new QWidget(this);
|
||||
{
|
||||
home_layout = new QHBoxLayout(home_widget);
|
||||
home_layout->setContentsMargins(0, 0, 0, 0);
|
||||
home_layout->setSpacing(30);
|
||||
|
||||
#ifndef SUNNYPILOT
|
||||
// left: PrimeAdWidget
|
||||
QStackedWidget *left_widget = new QStackedWidget(this);
|
||||
QVBoxLayout *left_prime_layout = new QVBoxLayout();
|
||||
left_prime_layout->setContentsMargins(0, 0, 0, 0);
|
||||
QWidget *prime_user = new PrimeUserWidget();
|
||||
prime_user->setStyleSheet(R"(
|
||||
border-radius: 10px;
|
||||
background-color: #333333;
|
||||
)");
|
||||
left_prime_layout->addWidget(prime_user);
|
||||
left_prime_layout->addStretch();
|
||||
left_widget->addWidget(new LayoutWidget(left_prime_layout));
|
||||
left_widget->addWidget(new PrimeAdWidget);
|
||||
left_widget->setStyleSheet("border-radius: 10px;");
|
||||
|
||||
connect(uiState()->prime_state, &PrimeState::changed, [left_widget]() {
|
||||
left_widget->setCurrentIndex(uiState()->prime_state->isSubscribed() ? 0 : 1);
|
||||
});
|
||||
|
||||
home_layout->addWidget(left_widget, 1);
|
||||
#endif
|
||||
|
||||
// right: ExperimentalModeButton, SetupWidget
|
||||
QWidget* right_widget = new QWidget(this);
|
||||
QVBoxLayout* right_column = new QVBoxLayout(right_widget);
|
||||
right_column->setContentsMargins(0, 0, 0, 0);
|
||||
right_widget->setFixedWidth(750);
|
||||
right_column->setSpacing(30);
|
||||
|
||||
ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this);
|
||||
QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings);
|
||||
right_column->addWidget(experimental_mode, 1);
|
||||
|
||||
SetupWidget *setup_widget = new SetupWidget;
|
||||
QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings);
|
||||
right_column->addWidget(setup_widget, 1);
|
||||
|
||||
home_layout->addWidget(right_widget, 1);
|
||||
}
|
||||
center_layout->addWidget(home_widget);
|
||||
|
||||
// add update & alerts widgets
|
||||
update_widget = new UpdateAlert();
|
||||
QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
|
||||
center_layout->addWidget(update_widget);
|
||||
alerts_widget = new OffroadAlert();
|
||||
QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
|
||||
center_layout->addWidget(alerts_widget);
|
||||
|
||||
main_layout->addLayout(center_layout, 1);
|
||||
|
||||
// set up refresh timer
|
||||
timer = new QTimer(this);
|
||||
timer->callOnTimeout(this, &OffroadHome::refresh);
|
||||
|
||||
setStyleSheet(R"(
|
||||
* {
|
||||
color: white;
|
||||
}
|
||||
OffroadHome {
|
||||
background-color: black;
|
||||
}
|
||||
OffroadHome > QPushButton {
|
||||
padding: 15px 30px;
|
||||
border-radius: 5px;
|
||||
font-size: 40px;
|
||||
font-weight: 500;
|
||||
}
|
||||
OffroadHome > QLabel {
|
||||
font-size: 55px;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
void OffroadHome::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(10 * 1000);
|
||||
}
|
||||
|
||||
void OffroadHome::hideEvent(QHideEvent *event) {
|
||||
timer->stop();
|
||||
}
|
||||
|
||||
void OffroadHome::refresh() {
|
||||
version->setText(getBrand() + " " + QString::fromStdString(params.get("UpdaterCurrentDescription")));
|
||||
|
||||
bool updateAvailable = update_widget->refresh();
|
||||
int alerts = alerts_widget->refresh();
|
||||
|
||||
// pop-up new notification
|
||||
int idx = center_layout->currentIndex();
|
||||
if (!updateAvailable && !alerts) {
|
||||
idx = 0;
|
||||
} else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) {
|
||||
idx = 1;
|
||||
} else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) {
|
||||
idx = 2;
|
||||
}
|
||||
center_layout->setCurrentIndex(idx);
|
||||
|
||||
update_notif->setVisible(updateAvailable);
|
||||
alert_notif->setVisible(alerts);
|
||||
if (alerts) {
|
||||
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
|
||||
}
|
||||
}
|
||||
59
selfdrive/ui/qt/offroad/offroad_home.h
Normal file
59
selfdrive/ui/qt/offroad/offroad_home.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/body.h"
|
||||
#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/sidebar.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/prime.h"
|
||||
#define OnroadWindow OnroadWindowSP
|
||||
#define LayoutWidget LayoutWidgetSP
|
||||
#define Sidebar SidebarSP
|
||||
#define ElidedLabel ElidedLabelSP
|
||||
#define SetupWidget SetupWidgetSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/qt/onroad/onroad_home.h"
|
||||
#include "selfdrive/ui/qt/sidebar.h"
|
||||
#include "selfdrive/ui/qt/widgets/prime.h"
|
||||
#endif
|
||||
|
||||
class OffroadHome : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OffroadHome(QWidget* parent = 0);
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
|
||||
protected:
|
||||
QHBoxLayout *home_layout;
|
||||
QHBoxLayout *header_layout;
|
||||
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
Params params;
|
||||
|
||||
QTimer* timer;
|
||||
ElidedLabel* version;
|
||||
QStackedLayout* center_layout;
|
||||
UpdateAlert *update_widget;
|
||||
OffroadAlert* alerts_widget;
|
||||
QPushButton* alert_notif;
|
||||
QPushButton* update_notif;
|
||||
};
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class CruiseLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
|
||||
items = [
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -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.
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import button_item
|
||||
|
||||
from openpilot.system.ui.sunnypilot.widgets.html_render import HtmlModalSP
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
|
||||
PREBUILT_PATH = os.path.join(Paths.comma_home(), "prebuilt") if PC else "/data/openpilot/prebuilt"
|
||||
|
||||
|
||||
class DeveloperLayoutSP(DeveloperLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.error_log_path = os.path.join(Paths.crash_log_root(), "error.log")
|
||||
self._is_release_branch: bool = self._is_release or ui_state.params.get_bool("IsReleaseSpBranch")
|
||||
self._is_development_branch: bool = ui_state.params.get_bool("IsTestedBranch") or ui_state.params.get_bool("IsDevelopmentBranch")
|
||||
self._initialize_items()
|
||||
|
||||
for item in self.items:
|
||||
self._scroller.add_widget(item)
|
||||
|
||||
def _initialize_items(self):
|
||||
self.show_advanced_controls = toggle_item_sp(tr("Show Advanced Controls"),
|
||||
tr("Toggle visibility of advanced sunnypilot controls.<br>This only changes the visibility of the toggles; " +
|
||||
"it does not change the actual enabled/disabled state."), param="ShowAdvancedControls")
|
||||
|
||||
self.enable_github_runner_toggle = toggle_item_sp(tr("GitHub Runner Service"), tr("Enables or disables the GitHub runner service."),
|
||||
param="EnableGithubRunner")
|
||||
|
||||
self.enable_copyparty_toggle = toggle_item_sp(tr("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 its IP address."), param="EnableCopyparty")
|
||||
|
||||
self.prebuilt_toggle = toggle_item_sp(tr("Quickboot Mode"), "", param="QuickBootToggle", callback=self._on_prebuilt_toggled)
|
||||
|
||||
self.error_log_btn = button_item(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."), callback=self._on_error_log_clicked)
|
||||
|
||||
self.items: list = [self.show_advanced_controls, self.enable_github_runner_toggle, self.enable_copyparty_toggle, self.prebuilt_toggle, self.error_log_btn,]
|
||||
|
||||
@staticmethod
|
||||
def _on_prebuilt_toggled(state):
|
||||
if state:
|
||||
Path(PREBUILT_PATH).touch(exist_ok=True)
|
||||
else:
|
||||
os.remove(PREBUILT_PATH)
|
||||
ui_state.params.put_bool("QuickBootToggle", state)
|
||||
|
||||
def _on_delete_confirm(self, result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
if os.path.exists(self.error_log_path):
|
||||
os.remove(self.error_log_path)
|
||||
|
||||
def _on_error_log_closed(self, result, log_exists):
|
||||
if result == DialogResult.CONFIRM and log_exists:
|
||||
dialog2 = ConfirmDialog(tr("Would you like to delete this log?"), tr("Yes"), tr("No"), rich=False)
|
||||
gui_app.set_modal_overlay(dialog2, callback=self._on_delete_confirm)
|
||||
|
||||
def _on_error_log_clicked(self):
|
||||
text = ""
|
||||
if os.path.exists(self.error_log_path):
|
||||
text = f"<b>{datetime.datetime.fromtimestamp(os.path.getmtime(self.error_log_path)).strftime('%d-%b-%Y %H:%M:%S').upper()}</b><br><br>"
|
||||
try:
|
||||
with open(self.error_log_path) as file:
|
||||
text += file.read()
|
||||
except Exception:
|
||||
pass
|
||||
dialog = HtmlModalSP(text=text, callback=lambda result: self._on_error_log_closed(result, os.path.exists(self.error_log_path)))
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
|
||||
def _update_state(self):
|
||||
disable_updates = ui_state.params.get_bool("DisableUpdates")
|
||||
show_advanced = ui_state.params.get_bool("ShowAdvancedControls")
|
||||
|
||||
if (prebuilt_file := os.path.exists(PREBUILT_PATH)) != ui_state.params.get_bool("QuickBootToggle"):
|
||||
ui_state.params.put_bool("QuickBootToggle", prebuilt_file)
|
||||
self.prebuilt_toggle.action_item.set_state(prebuilt_file)
|
||||
|
||||
self.prebuilt_toggle.set_visible(show_advanced and not (self._is_release_branch or self._is_development_branch))
|
||||
self.prebuilt_toggle.action_item.set_enabled(disable_updates)
|
||||
|
||||
if disable_updates:
|
||||
self.prebuilt_toggle.set_description(tr("When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it " +
|
||||
"removes the prebuilt file so compilation of locally edited cpp files can be made."))
|
||||
else:
|
||||
self.prebuilt_toggle.set_description(tr("Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first."))
|
||||
|
||||
self.enable_copyparty_toggle.set_visible(show_advanced)
|
||||
self.enable_github_runner_toggle.set_visible(show_advanced and not self._is_release_branch)
|
||||
self.error_log_btn.set_visible(not self._is_release_branch)
|
||||
@@ -1,12 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
|
||||
|
||||
class DeviceLayoutSP(DeviceLayout):
|
||||
def __init__(self):
|
||||
DeviceLayout.__init__(self)
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class DisplayLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -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.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets.toggle import ON_COLOR
|
||||
|
||||
from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
|
||||
|
||||
|
||||
class ModelsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.model_manager = None
|
||||
self.download_status = None
|
||||
self.prev_download_status = None
|
||||
self.model_dialog = None
|
||||
self.last_cache_calc_time = 0
|
||||
|
||||
self._initialize_items()
|
||||
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]:
|
||||
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
||||
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
self.current_model_item = ListItemSP(
|
||||
title=tr("Current Model"),
|
||||
description="",
|
||||
action_item=NoElideButtonAction(tr("SELECT")),
|
||||
callback=self._handle_current_model_clicked
|
||||
)
|
||||
|
||||
self.supercombo_label = progress_item(tr("Driving Model"))
|
||||
self.vision_label = progress_item(tr("Vision Model"))
|
||||
self.policy_label = progress_item(tr("Policy Model"))
|
||||
|
||||
self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "",
|
||||
lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0),
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Fetching Latest Models")))))
|
||||
|
||||
self.clear_cache_item = ListItemSP(
|
||||
title=tr("Clear Model Cache"),
|
||||
description="",
|
||||
action_item=NoElideButtonAction(tr("CLEAR")),
|
||||
callback=self._clear_cache
|
||||
)
|
||||
|
||||
self.cancel_download_item = button_item(tr("Cancel Download"), tr("Cancel"), "", lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||
|
||||
self.lane_turn_value_control = option_item_sp(tr("Adjust Lane Turn Speed"), "LaneTurnValue", 500, 2000,
|
||||
tr("Set the maximum speed for lane turn desires. Default is 19 mph."),
|
||||
int(round(100 / CV.MPH_TO_KPH)), None, True, "", style.BUTTON_WIDTH, None, True,
|
||||
lambda v: f"{int(round(v / 100 * (CV.MPH_TO_KPH if ui_state.is_metric else 1)))}" +
|
||||
f" {'km/h' if ui_state.is_metric else 'mph'}")
|
||||
|
||||
self.lane_turn_desire_toggle = toggle_item_sp(tr("Use Lane Turn Desires"),
|
||||
tr("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."),
|
||||
param="LaneTurnDesire")
|
||||
|
||||
self.delay_control = option_item_sp(tr("Adjust Software Delay"), "LagdToggleDelay", 5, 50,
|
||||
tr("Adjust the software delay when Live Learning Steer Delay is toggled off. The default software delay value is 0.2"),
|
||||
1, None, True, "", style.BUTTON_WIDTH, None, True, lambda v: f"{v / 100:.2f}s")
|
||||
|
||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||
|
||||
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||
self.policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle,
|
||||
self.lane_turn_value_control, self.lagd_toggle, self.delay_control]
|
||||
|
||||
def _update_lagd_description(self, lagd_toggle: bool):
|
||||
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
||||
"Keeping this on provides the stock openpilot experience.")
|
||||
if lagd_toggle:
|
||||
desc += f"<br>{tr('Live Steer Delay:')} {ui_state.sm['liveDelay'].lateralDelay:.3f} s"
|
||||
elif ui_state.CP:
|
||||
sw = float(ui_state.params.get("LagdToggleDelay", "0.2"))
|
||||
cp = ui_state.CP.steerActuatorDelay
|
||||
desc += f"<br>{tr('Actuator Delay:')} {cp:.2f} s + {tr('Software Delay:')} {sw:.2f} s = {tr('Total Delay:')} {cp + sw:.2f} s"
|
||||
self.lagd_toggle.set_description(desc)
|
||||
|
||||
def _is_downloading(self):
|
||||
return (self.model_manager and self.model_manager.selectedBundle and
|
||||
self.model_manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading)
|
||||
|
||||
@staticmethod
|
||||
def _calculate_cache_size():
|
||||
cache_size = 0.0
|
||||
if os.path.exists(CUSTOM_MODEL_PATH):
|
||||
cache_size = sum(os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) for file in os.listdir(CUSTOM_MODEL_PATH)) / (1024**2)
|
||||
return cache_size
|
||||
|
||||
def _clear_cache(self):
|
||||
def _callback(response):
|
||||
if response == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ModelManager_ClearCache", True)
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("This will delete ALL downloaded models from the cache except the currently active model. Are you sure?"),
|
||||
tr("Clear Cache")), callback=_callback)
|
||||
|
||||
def _handle_bundle_download_progress(self):
|
||||
labels = {custom.ModelManagerSP.Model.Type.supercombo: self.supercombo_label,
|
||||
custom.ModelManagerSP.Model.Type.vision: self.vision_label,
|
||||
custom.ModelManagerSP.Model.Type.policy: self.policy_label}
|
||||
for label in labels.values():
|
||||
label.set_visible(False)
|
||||
self.cancel_download_item.set_visible(False)
|
||||
|
||||
if not self.model_manager or (not self.model_manager.selectedBundle and not self.model_manager.activeBundle):
|
||||
return
|
||||
|
||||
bundle = self.model_manager.selectedBundle if self._is_downloading() or (
|
||||
self.model_manager.selectedBundle and self.model_manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed
|
||||
) else self.model_manager.activeBundle
|
||||
if not bundle:
|
||||
return
|
||||
|
||||
self.download_status = bundle.status
|
||||
status_changed = self.prev_download_status != self.download_status
|
||||
self.prev_download_status = self.download_status
|
||||
|
||||
self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and bool(ui_state.params.get("ModelManager_DownloadIndex")))
|
||||
|
||||
if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5:
|
||||
self.last_cache_calc_time = current_time
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
device.reset_interactive_timeout()
|
||||
|
||||
for model in bundle.models:
|
||||
if label := labels.get(getattr(model.type, 'raw', model.type)):
|
||||
label.set_visible(True)
|
||||
p = model.artifact.downloadProgress
|
||||
text, show, color = f"pending - {bundle.displayName}", False, rl.GRAY
|
||||
if p.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
text, show = f"{int(p.progress)}% - {bundle.displayName}", True
|
||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded, custom.ModelManagerSP.DownloadStatus.cached):
|
||||
status_text = tr("from cache" if p.status == custom.ModelManagerSP.DownloadStatus.cached else "downloaded")
|
||||
text, color = f"{bundle.displayName} - {status_text if status_changed else tr('ready')}", ON_COLOR
|
||||
elif p.status == custom.ModelManagerSP.DownloadStatus.failed:
|
||||
text, color = f"download failed - {bundle.displayName}", rl.RED
|
||||
label.action_item.update(p.progress, text, show, color)
|
||||
|
||||
@staticmethod
|
||||
def _show_reset_params_dialog():
|
||||
def _callback(response):
|
||||
if response == DialogResult.CONFIRM:
|
||||
ui_state.params.remove("CalibrationParams")
|
||||
ui_state.params.remove("LiveTorqueParameters")
|
||||
msg = tr("Model download has started in the background. We suggest resetting calibration. Would you like to do that now?")
|
||||
gui_app.set_modal_overlay(ConfirmDialog(msg, tr("Reset Calibration")), callback=_callback)
|
||||
|
||||
def _on_model_selected(self, result):
|
||||
if result != DialogResult.CONFIRM:
|
||||
return
|
||||
selected_ref = self.model_dialog.selection_ref
|
||||
if selected_ref == "Default":
|
||||
ui_state.params.remove("ModelManager_ActiveBundle")
|
||||
self._show_reset_params_dialog()
|
||||
elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None):
|
||||
ui_state.params.put("ModelManager_DownloadIndex", selected_bundle.index)
|
||||
if self.model_manager.activeBundle and selected_bundle.generation != self.model_manager.activeBundle.generation:
|
||||
self._show_reset_params_dialog()
|
||||
self.model_dialog = None
|
||||
|
||||
@staticmethod
|
||||
def _bundle_to_node(bundle):
|
||||
return TreeNode(bundle.ref, {'display_name': bundle.displayName, 'short_name': bundle.internalName})
|
||||
|
||||
def _get_folders(self, favorites):
|
||||
bundles = self.model_manager.availableBundles
|
||||
folders = {}
|
||||
for bundle in bundles:
|
||||
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
|
||||
|
||||
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': tr("Default Model"), 'short_name': "Default"})])]
|
||||
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
|
||||
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
|
||||
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
|
||||
folders_list.append(TreeFolder(name, [self._bundle_to_node(bundle) for bundle in folder_bundles]))
|
||||
|
||||
if favorites and (fav_bundles := [bundle for bundle in bundles if bundle.ref in favorites]):
|
||||
folders_list.insert(1, TreeFolder("Favorites", [self._bundle_to_node(bundle) for bundle in fav_bundles]))
|
||||
return folders_list
|
||||
|
||||
def _handle_current_model_clicked(self):
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
folders_list = self._get_folders(favorites)
|
||||
|
||||
active_ref = self.model_manager.activeBundle.ref if self.model_manager.activeBundle else "Default"
|
||||
self.model_dialog = TreeOptionDialog(tr("Select a Model"), folders_list, active_ref, "ModelManager_Favs",
|
||||
get_folders_fn=self._get_folders, on_exit=self._on_model_selected)
|
||||
gui_app.set_modal_overlay(self.model_dialog, callback=self._on_model_selected)
|
||||
|
||||
def _update_state(self):
|
||||
advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls")
|
||||
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
||||
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
||||
|
||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||
self.lagd_toggle.action_item.set_state(live_delay)
|
||||
self.delay_control.set_visible(not live_delay and advanced_controls)
|
||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||
if self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||
self.lane_turn_value_control.action_item.value_change_step = new_step
|
||||
|
||||
self._update_lagd_description(live_delay)
|
||||
self.model_manager = ui_state.sm["modelManagerSP"]
|
||||
self._handle_bundle_download_progress()
|
||||
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else tr("Default Model")
|
||||
self.current_model_item.action_item.set_value(active_name)
|
||||
|
||||
if not ui_state.is_offroad():
|
||||
self.current_model_item.action_item.set_enabled(False)
|
||||
self.current_model_item.set_description(tr("Only available when vehicle is off, or always offroad mode is on"))
|
||||
else:
|
||||
self.current_model_item.action_item.set_enabled(True)
|
||||
self.current_model_item.set_description("")
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class NavigationLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -1,46 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.network import NetworkUI, PanelType
|
||||
|
||||
|
||||
class NetworkUISP(NetworkUI):
|
||||
def __init__(self, wifi_manager):
|
||||
super().__init__(wifi_manager)
|
||||
|
||||
self.scan_button = Button(tr("Scan"), self._scan_clicked, button_style=ButtonStyle.NORMAL, font_size=60, border_radius=30)
|
||||
self.scan_button.set_rect(rl.Rectangle(0, 0, 400, 100))
|
||||
|
||||
self._scanning = False
|
||||
self._wifi_manager.add_callbacks(networks_updated=self._on_networks_updated)
|
||||
|
||||
def _scan_clicked(self):
|
||||
self._scanning = True
|
||||
self.scan_button.set_text(tr("Scanning..."))
|
||||
self.scan_button.set_enabled(False)
|
||||
|
||||
threading.Thread(target=self._wifi_manager._update_networks, daemon=True).start()
|
||||
self._wifi_manager._request_scan()
|
||||
self._wifi_manager._last_network_update = time.monotonic()
|
||||
|
||||
def _on_networks_updated(self, networks):
|
||||
if self._scanning:
|
||||
self._scanning = False
|
||||
self.scan_button.set_text(tr("Scan"))
|
||||
self.scan_button.set_enabled(True)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
super()._render(rect)
|
||||
|
||||
if self._current_panel == PanelType.WIFI:
|
||||
self.scan_button.set_position(self._rect.x, self._rect.y + 20)
|
||||
self.scan_button.render()
|
||||
@@ -1,232 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
import platform
|
||||
import requests
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import time_ago
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import text_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeFolder, TreeNode, TreeOptionDialog
|
||||
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
|
||||
|
||||
MAP_PATH = Path(Paths.mapd_root()) / "offline"
|
||||
|
||||
|
||||
class OSMLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._current_percent = 0
|
||||
self._last_map_size_update = 0
|
||||
self._mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else ui_state.params
|
||||
self._initialize_items()
|
||||
self._update_map_size()
|
||||
self._progress.set_visible(False)
|
||||
self._state_btn.set_visible(False)
|
||||
self._mapd_version.action_item.set_text(ui_state.params.get("MapdVersion") or "Loading...")
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
self._mapd_version = text_item(tr("Mapd Version"), lambda: ui_state.params.get("MapdVersion") or "Loading...")
|
||||
self._delete_maps_btn = ListItemSP(tr("Downloaded Maps"), action_item=NoElideButtonAction(tr("DELETE"), enabled=True), callback=self._delete_maps)
|
||||
self._progress = progress_item(tr("Downloading Map"))
|
||||
self._update_btn = ListItemSP(tr("Database Update"), action_item=NoElideButtonAction(tr("CHECK"), enabled=True), callback=self._update_db)
|
||||
self._country_btn = ListItemSP(tr("Country"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("Country"))
|
||||
self._state_btn = ListItemSP(tr("State"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("State"))
|
||||
|
||||
self.items = [self._mapd_version, self._delete_maps_btn, self._progress, self._update_btn, self._country_btn, self._state_btn]
|
||||
|
||||
def _show_confirm(self, msg, confirm_text, func):
|
||||
gui_app.set_modal_overlay(ConfirmDialog(msg, confirm_text), lambda res: func() if res == DialogResult.CONFIRM else None)
|
||||
|
||||
def calculate_size(self):
|
||||
total_size = 0
|
||||
directories_to_scan = [MAP_PATH] if MAP_PATH.exists() else []
|
||||
while directories_to_scan:
|
||||
try:
|
||||
for entry in os.scandir(directories_to_scan.pop()):
|
||||
if entry.is_file():
|
||||
total_size += entry.stat().st_size
|
||||
elif entry.is_dir():
|
||||
directories_to_scan.append(entry.path)
|
||||
except OSError:
|
||||
pass
|
||||
self._delete_maps_btn.action_item.set_value(f"{total_size / 1024 ** 2:.2f} MB" if total_size < 1024 ** 3 else f"{total_size / 1024 ** 3:.2f} GB")
|
||||
|
||||
def _update_map_size(self):
|
||||
threading.Thread(target=self.calculate_size, daemon=True).start()
|
||||
|
||||
def _do_delete_maps(self):
|
||||
if MAP_PATH.exists():
|
||||
shutil.rmtree(MAP_PATH)
|
||||
|
||||
for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"):
|
||||
ui_state.params.remove(param)
|
||||
|
||||
self._delete_maps_btn.action_item.set_enabled(True)
|
||||
self._delete_maps_btn.action_item.set_text(tr("DELETE"))
|
||||
self._update_map_size()
|
||||
|
||||
def _on_confirm_delete_maps(self):
|
||||
self._delete_maps_btn.action_item.set_enabled(False)
|
||||
self._delete_maps_btn.action_item.set_text("DELETING...")
|
||||
threading.Thread(target=self._do_delete_maps).start()
|
||||
|
||||
def _delete_maps(self):
|
||||
self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"),
|
||||
tr("Yes, delete all maps"), self._on_confirm_delete_maps)
|
||||
|
||||
def _update_db(self):
|
||||
self._show_confirm(tr("This will start the download process and it might take a while to complete."), tr("Start Download"),
|
||||
lambda: ui_state.params.put_bool("OsmDbUpdatesCheck", True))
|
||||
|
||||
def _select_region(self, region_type):
|
||||
is_country = region_type == "Country"
|
||||
btn = self._country_btn if is_country else self._state_btn
|
||||
btn.action_item.set_enabled(False)
|
||||
btn.action_item.set_text(tr("FETCHING..."))
|
||||
threading.Thread(target=self._do_select_region, args=(region_type, btn)).start()
|
||||
|
||||
def _handle_region_selection(self, region_type, locations, key, res, ref):
|
||||
if res != DialogResult.CONFIRM or not ref:
|
||||
if region_type == "State" and res == DialogResult.CANCEL:
|
||||
if ui_state.params.get("OsmLocationName") == "US" and not ui_state.params.get("OsmStateName"):
|
||||
ui_state.params.remove("OsmLocationName")
|
||||
ui_state.params.remove("OsmLocationTitle")
|
||||
ui_state.params.remove("OsmLocal")
|
||||
self._update_labels()
|
||||
return
|
||||
|
||||
if region_type == "Country":
|
||||
ui_state.params.put_bool("OsmLocal", True)
|
||||
ui_state.params.remove("OsmStateName")
|
||||
ui_state.params.remove("OsmStateTitle")
|
||||
|
||||
ui_state.params.put(f"{key}Name", ref)
|
||||
name = next((n.data['display_name'] for n in locations if n.ref == ref), ref)
|
||||
ui_state.params.put(f"{key}Title", name)
|
||||
|
||||
if ref == "US" and region_type == "Country":
|
||||
self._select_region("State")
|
||||
else:
|
||||
self._update_db()
|
||||
|
||||
def _do_select_region(self, region_type, btn):
|
||||
base_url = "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/"
|
||||
url = base_url + ("nation_bounding_boxes.json" if region_type == "Country" else "us_states_bounding_boxes.json")
|
||||
try:
|
||||
data = requests.get(url, timeout=10).json()
|
||||
locations = sorted([TreeNode(ref=k, data={'display_name': v['full_name']}) for k, v in data.items()], key=lambda n: n.data['display_name'])
|
||||
except Exception:
|
||||
locations = []
|
||||
|
||||
if region_type == "State":
|
||||
locations.insert(0, TreeNode(ref="All", data={'display_name': tr("All states (~6.0 GB)")}))
|
||||
|
||||
btn.action_item.set_enabled(True)
|
||||
btn.action_item.set_text(tr("SELECT"))
|
||||
|
||||
key = "OsmLocation" if region_type == "Country" else "OsmState"
|
||||
current = ui_state.params.get(f"{key}Name") or ""
|
||||
|
||||
dialog = TreeOptionDialog(tr(f"Select {region_type}"), [TreeFolder(folder="", nodes=locations)], current_ref=current, search_prompt="Perform a search")
|
||||
dialog.on_exit = lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref)
|
||||
gui_app.set_modal_overlay(dialog, callback=lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref))
|
||||
|
||||
def _update_labels(self):
|
||||
downloading = bool(self._mem_params.get("OSMDownloadLocations"))
|
||||
self._country_btn.set_enabled(not downloading)
|
||||
self._state_btn.set_enabled(not downloading)
|
||||
self._state_btn.set_visible(ui_state.params.get("OsmLocationName") == "US")
|
||||
self._update_btn.set_visible(bool(ui_state.params.get("OsmLocationName")))
|
||||
|
||||
self._country_btn.action_item.set_value(ui_state.params.get("OsmLocationTitle") or "")
|
||||
self._state_btn.action_item.set_value(ui_state.params.get("OsmStateTitle") or "")
|
||||
|
||||
pending = ui_state.params.get_bool("OsmDbUpdatesCheck")
|
||||
if downloading or pending:
|
||||
if downloading:
|
||||
device.reset_interactive_timeout()
|
||||
self._update_map_size()
|
||||
self._progress.set_visible(True)
|
||||
progress = ui_state.params.get("OSMDownloadProgress")
|
||||
total = progress.get('total_files', 0) if progress else 0
|
||||
done = progress.get('downloaded_files', 0) if progress else 0
|
||||
failed = total > 0 and not downloading and done < total
|
||||
|
||||
if total > 0:
|
||||
progress_perc = max(0.0, min(100.0, (done / total) * 100.0))
|
||||
else:
|
||||
progress_perc = 0.0
|
||||
|
||||
if failed:
|
||||
text = "0% - Downloading Maps"
|
||||
btn_text = tr("Error: Invalid download. Retry.")
|
||||
self._current_percent = 0.0
|
||||
elif total > 0 and downloading:
|
||||
self._current_percent = progress_perc
|
||||
perc_int = int(progress_perc)
|
||||
text = f"{perc_int}% - Downloading Maps"
|
||||
btn_text = f"{done}/{total} ({perc_int}%)"
|
||||
else:
|
||||
self._current_percent = 0.0
|
||||
text = "0% - Downloading Maps"
|
||||
btn_text = tr("Downloading Maps...")
|
||||
|
||||
self._progress.action_item.update(self._current_percent, text, show_progress=total > 0 and downloading and not failed)
|
||||
self._update_btn.action_item.set_enabled(not downloading) # TODO-SP: introduce CANCEL database download with mapd
|
||||
self._update_btn.action_item.set_value(btn_text)
|
||||
self._country_btn.action_item.set_enabled(not downloading)
|
||||
self._state_btn.action_item.set_enabled(not downloading)
|
||||
self._delete_maps_btn.action_item.set_enabled(not downloading)
|
||||
else:
|
||||
self._progress.set_visible(False)
|
||||
self._update_btn.action_item.set_enabled(True)
|
||||
self._country_btn.action_item.set_enabled(True)
|
||||
self._state_btn.action_item.set_enabled(True)
|
||||
self._delete_maps_btn.action_item.set_enabled(True)
|
||||
|
||||
ts = ui_state.params.get("OsmDownloadedDate")
|
||||
dt: datetime.datetime | None = None
|
||||
|
||||
if ts:
|
||||
try:
|
||||
ts_f = float(ts)
|
||||
if ts_f > 0:
|
||||
dt = datetime.datetime.fromtimestamp(ts_f, tz=datetime.UTC)
|
||||
except (ValueError, TypeError):
|
||||
dt = None
|
||||
|
||||
formatted = time_ago(dt)
|
||||
self._update_btn.action_item.set_value(tr("Last checked {}").format(formatted))
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
|
||||
def _update_state(self):
|
||||
now = monotonic()
|
||||
if now - self._last_map_size_update >= 1.0:
|
||||
self._last_map_size_update = now
|
||||
self._update_labels()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
@@ -1,202 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
|
||||
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
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.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle import VehicleLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
|
||||
|
||||
# from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
|
||||
|
||||
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
|
||||
ICON_SIZE = 70
|
||||
|
||||
OP.PanelType = IntEnum( # type: ignore
|
||||
"PanelType",
|
||||
[es.name for es in OP.PanelType] + [
|
||||
"SUNNYLINK",
|
||||
"MODELS",
|
||||
"STEERING",
|
||||
"CRUISE",
|
||||
"VISUALS",
|
||||
"DISPLAY",
|
||||
"OSM",
|
||||
"NAVIGATION",
|
||||
"TRIPS",
|
||||
"VEHICLE",
|
||||
],
|
||||
start=0,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PanelInfo(OP.PanelInfo):
|
||||
icon: str = ""
|
||||
|
||||
|
||||
class NavButton(Widget):
|
||||
def __init__(self, parent, p_type, p_info):
|
||||
super().__init__()
|
||||
self.parent = parent
|
||||
self.panel_type = p_type
|
||||
self.panel_info = p_info
|
||||
|
||||
def _render(self, rect):
|
||||
is_selected = self.panel_type == self.parent._current_panel
|
||||
text_color = OP.TEXT_SELECTED if is_selected else OP.TEXT_NORMAL
|
||||
content_x = rect.x + 90
|
||||
text_size = measure_text_cached(self.parent._font_medium, self.panel_info.name, 65)
|
||||
|
||||
# Draw background if selected
|
||||
if is_selected:
|
||||
self.container_rect = rl.Rectangle(
|
||||
content_x - 50, rect.y, OP.SIDEBAR_WIDTH - 50, OP.NAV_BTN_HEIGHT
|
||||
)
|
||||
rl.draw_rectangle_rounded(self.container_rect, 0.2, 5, OP.CLOSE_BTN_COLOR)
|
||||
|
||||
if self.panel_info.icon:
|
||||
icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
|
||||
rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2),
|
||||
rl.WHITE)
|
||||
content_x += ICON_SIZE + 20
|
||||
|
||||
# Draw button text (right-aligned)
|
||||
text_pos = rl.Vector2(
|
||||
content_x,
|
||||
rect.y + (OP.NAV_BTN_HEIGHT - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self.parent._font_medium, self.panel_info.name, text_pos, 55, 0, text_color)
|
||||
|
||||
# Store button rect for click detection
|
||||
self.panel_info.button_rect = rect
|
||||
|
||||
|
||||
class SettingsLayoutSP(OP.SettingsLayout):
|
||||
def __init__(self):
|
||||
OP.SettingsLayout.__init__(self)
|
||||
self._nav_items: list[Widget] = []
|
||||
|
||||
# Create sidebar scroller
|
||||
self._sidebar_scroller = Scroller([], spacing=0, line_separator=False, pad_end=False)
|
||||
|
||||
# Panel configuration
|
||||
wifi_manager = WifiManager()
|
||||
wifi_manager.set_active(False)
|
||||
|
||||
self._panels = {
|
||||
OP.PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_home.png"),
|
||||
OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUISP(wifi_manager), icon="icons/network.png"),
|
||||
OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/wifi_strength_full.png"),
|
||||
OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
|
||||
OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
|
||||
OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_models.png"),
|
||||
OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
|
||||
OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"),
|
||||
OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
|
||||
OP.PanelType.DISPLAY: PanelInfo(tr_noop("Display"), DisplayLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_display.png"),
|
||||
OP.PanelType.OSM: PanelInfo(tr_noop("OSM"), OSMLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
|
||||
# OP.PanelType.NAVIGATION: PanelInfo(tr_noop("Navigation"), NavigationLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
|
||||
OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
|
||||
OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
|
||||
OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"),
|
||||
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayoutSP(), icon="icons/shell.png"),
|
||||
}
|
||||
|
||||
def _draw_sidebar(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rec(rect, OP.SIDEBAR_COLOR)
|
||||
|
||||
# Close button
|
||||
close_btn_rect = rl.Rectangle(
|
||||
rect.x + style.ITEM_PADDING * 3, rect.y + style.ITEM_PADDING * 2, style.CLOSE_BTN_SIZE, style.CLOSE_BTN_SIZE
|
||||
)
|
||||
|
||||
pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
rl.check_collision_point_rec(rl.get_mouse_position(), close_btn_rect))
|
||||
close_color = OP.CLOSE_BTN_PRESSED if pressed else OP.CLOSE_BTN_COLOR
|
||||
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# Store close button rect for click detection
|
||||
self._close_btn_rect = close_btn_rect
|
||||
|
||||
# Navigation buttons with scroller
|
||||
if not self._nav_items:
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
nav_button = NavButton(self, panel_type, panel_info)
|
||||
nav_button.rect.width = rect.width - 100 # Full width minus padding
|
||||
nav_button.rect.height = OP.NAV_BTN_HEIGHT
|
||||
self._nav_items.append(nav_button)
|
||||
self._sidebar_scroller.add_widget(nav_button)
|
||||
|
||||
# Draw navigation section with scroller
|
||||
nav_rect = rl.Rectangle(
|
||||
rect.x,
|
||||
self._close_btn_rect.height + style.ITEM_PADDING * 4, # Starting Y position for nav items
|
||||
rect.width,
|
||||
rect.height - 300 # Remaining height after close button
|
||||
)
|
||||
|
||||
if self._nav_items:
|
||||
self._sidebar_scroller.render(nav_rect)
|
||||
return
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
|
||||
# Check close button
|
||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
return True
|
||||
|
||||
# Check navigation buttons
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect) and self._sidebar_scroller.scroll_panel.is_touch_valid():
|
||||
self.set_current_panel(panel_type)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._panels[self._current_panel].instance.show_event()
|
||||
self._sidebar_scroller.show_event()
|
||||
@@ -1,96 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import os
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.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.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
|
||||
|
||||
|
||||
DESCRIPTIONS = {
|
||||
'disable_updates_offroad': tr_noop(
|
||||
"When enabled, automatic software updates will be off.<br><b>This requires a reboot to take effect.</b>"
|
||||
),
|
||||
'disable_updates_onroad': tr_noop(
|
||||
"Please enable \"Always Offroad\" mode or turn off the vehicle to adjust these toggles."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class SoftwareLayoutSP(SoftwareLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.disable_updates_toggle = toggle_item_sp(
|
||||
lambda: tr("Disable Updates"),
|
||||
description="",
|
||||
initial_state=ui_state.params.get_bool("DisableUpdates"),
|
||||
callback=self._on_disable_updates_toggled,
|
||||
)
|
||||
self._scroller.add_widget(self.disable_updates_toggle)
|
||||
|
||||
def _handle_reboot(self, result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("DisableUpdates", self.disable_updates_toggle.action_item.get_state())
|
||||
ui_state.params.put_bool("DoReboot", True)
|
||||
else:
|
||||
self.disable_updates_toggle.action_item.set_state(ui_state.params.get_bool("DisableUpdates"))
|
||||
|
||||
def _on_disable_updates_toggled(self, enabled):
|
||||
dialog = ConfirmDialog(tr("System reboot required for changes to take effect. Reboot now?"), tr("Reboot"))
|
||||
gui_app.set_modal_overlay(dialog, callback=self._handle_reboot)
|
||||
|
||||
def _on_select_branch(self):
|
||||
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]
|
||||
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
top_level_branches = [current_git_branch, "release-mici", "release-tizi", "staging", "dev", "master"]
|
||||
|
||||
if HARDWARE.get_device_type() == "tici":
|
||||
top_level_branches = ["release-tici", "staging-tici"]
|
||||
branches = [b for b in branches if b.endswith("-tici")]
|
||||
|
||||
top_level_nodes = [TreeNode(b, {'display_name': b}) for b in top_level_branches if b in branches]
|
||||
remaining_branches = [b for b in branches if b not in top_level_branches]
|
||||
prebuilt_nodes = [TreeNode(b, {'display_name': b}) for b in remaining_branches if b.endswith("-prebuilt")]
|
||||
non_prebuilt_nodes = [TreeNode(b, {'display_name': b}) for b in remaining_branches if not b.endswith("-prebuilt")]
|
||||
|
||||
folders = [
|
||||
TreeFolder("", top_level_nodes),
|
||||
TreeFolder("Prebuilt Branches", prebuilt_nodes),
|
||||
TreeFolder("Non-Prebuilt Branches", non_prebuilt_nodes),
|
||||
]
|
||||
|
||||
def _on_branch_selected(result):
|
||||
if result == DialogResult.CONFIRM and self._branch_dialog is not None:
|
||||
selection = self._branch_dialog.selection_ref
|
||||
if 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
|
||||
|
||||
self._branch_dialog = TreeOptionDialog(tr("Select a branch"), folders, current_target, "",
|
||||
on_exit=_on_branch_selected)
|
||||
|
||||
gui_app.set_modal_overlay(self._branch_dialog, callback=_on_branch_selected)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
show_advanced = ui_state.params.get_bool("ShowAdvancedControls")
|
||||
self.disable_updates_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
self.disable_updates_toggle.set_visible(show_advanced)
|
||||
|
||||
disable_updates_desc = tr(DESCRIPTIONS["disable_updates_offroad"] if ui_state.is_offroad() else DESCRIPTIONS["disable_updates_onroad"])
|
||||
self.disable_updates_toggle.set_description(disable_updates_desc)
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class SteeringLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -1,343 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from cereal import custom
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.list_view import button_item, dual_button_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
import pyray as rl
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
|
||||
|
||||
|
||||
class SunnylinkHeader(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._title = UnifiedLabel(
|
||||
text="🚀 sunnylink 🚀",
|
||||
font_size=90,
|
||||
font_weight=FontWeight.AUDIOWIDE,
|
||||
text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
wrap_text=False,
|
||||
elide=False
|
||||
)
|
||||
|
||||
self._description = UnifiedLabel(
|
||||
text=tr("For secure backup, restore, and remote configuration"),
|
||||
font_size=40,
|
||||
font_weight=FontWeight.LIGHT,
|
||||
text_color=rl.Color(0, 255, 0, 255), # Green
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
wrap_text=True,
|
||||
elide=False
|
||||
)
|
||||
|
||||
self._sponsor_msg = UnifiedLabel(
|
||||
text=tr("Sponsorship isn't required for basic backup/restore") + "\n" +
|
||||
tr("Click the Sponsor button for more details"),
|
||||
font_size=35,
|
||||
font_weight=FontWeight.LIGHT,
|
||||
text_color=rl.Color(255, 165, 0, 255), # Orange
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
wrap_text=True,
|
||||
elide=False
|
||||
)
|
||||
|
||||
self._padding = 20
|
||||
self._spacing = 10
|
||||
|
||||
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
|
||||
super().set_parent_rect(parent_rect)
|
||||
|
||||
content_width = int(parent_rect.width - (self._padding * 2))
|
||||
|
||||
title_height = self._title.get_content_height(content_width)
|
||||
desc_height = self._description.get_content_height(content_width)
|
||||
sponsor_height = self._sponsor_msg.get_content_height(content_width)
|
||||
|
||||
total_height = (self._padding + title_height + self._spacing +
|
||||
desc_height + self._spacing + sponsor_height + self._padding)
|
||||
|
||||
self._rect.width = parent_rect.width
|
||||
self._rect.height = total_height
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
content_width = rect.width - (self._padding * 2)
|
||||
current_y = rect.y + self._padding
|
||||
|
||||
# Render title
|
||||
title_height = self._title.get_content_height(int(content_width))
|
||||
title_rect = rl.Rectangle(rect.x + self._padding, current_y, content_width, title_height)
|
||||
self._title.render(title_rect)
|
||||
current_y += title_height + self._spacing
|
||||
|
||||
# Render description
|
||||
desc_height = self._description.get_content_height(int(content_width))
|
||||
desc_rect = rl.Rectangle(rect.x + self._padding, current_y, content_width, desc_height)
|
||||
self._description.render(desc_rect)
|
||||
current_y += desc_height + self._spacing
|
||||
|
||||
# Render sponsor message
|
||||
sponsor_height = self._sponsor_msg.get_content_height(int(content_width))
|
||||
sponsor_rect = rl.Rectangle(rect.x + self._padding, current_y, content_width, sponsor_height)
|
||||
self._sponsor_msg.render(sponsor_rect)
|
||||
|
||||
|
||||
class SunnylinkDescriptionItem(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._description = UnifiedLabel(
|
||||
text="",
|
||||
font_size=40,
|
||||
font_weight=FontWeight.LIGHT,
|
||||
text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
wrap_text=True,
|
||||
elide=False,
|
||||
)
|
||||
self._padding = 20
|
||||
|
||||
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
|
||||
super().set_parent_rect(parent_rect)
|
||||
desc_height = self._description.get_content_height(int(parent_rect.width)) + self._padding * 2
|
||||
|
||||
self._rect.width = parent_rect.width
|
||||
self._rect.height = desc_height
|
||||
|
||||
def set_text(self, text: str):
|
||||
self._description.set_text(text)
|
||||
|
||||
def set_color(self, color: rl.Color):
|
||||
self._description.set_text_color(color)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
content_width = rect.width - (self._padding * 2)
|
||||
|
||||
desc_height = self._description.get_content_height(int(content_width))
|
||||
desc_rect = rl.Rectangle(rect.x + self._padding, rect.y, content_width, desc_height)
|
||||
self._description.render(desc_rect)
|
||||
|
||||
|
||||
class SunnylinkLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._sunnylink_pairing_dialog: SunnylinkPairingDialog | None = None
|
||||
self._restore_in_progress = False
|
||||
self._backup_in_progress = False
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=False, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
self._sunnylink_toggle = toggle_item_sp(
|
||||
title=tr("Enable sunnylink"),
|
||||
description=tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that."),
|
||||
param="SunnylinkEnabled",
|
||||
callback=self._sunnylink_toggle_callback
|
||||
)
|
||||
|
||||
self._sunnylink_description = SunnylinkDescriptionItem()
|
||||
self._sunnylink_description.set_visible(False)
|
||||
|
||||
self._sponsor_btn = button_item(
|
||||
title=tr("Sponsor Status"),
|
||||
button_text=tr("SPONSOR"),
|
||||
description=tr(
|
||||
"Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."),
|
||||
callback=lambda: self._handle_pair_btn(False)
|
||||
)
|
||||
self._pair_btn = button_item(
|
||||
title=tr("Pair GitHub Account"),
|
||||
button_text=tr("Not Paired"),
|
||||
description=tr(
|
||||
"Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink."),
|
||||
callback=lambda: self._handle_pair_btn(True)
|
||||
)
|
||||
self._sunnylink_uploader_toggle = toggle_item_sp(
|
||||
title=tr("Enable sunnylink uploader (infrastructure test)"),
|
||||
description=tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. ") +
|
||||
tr("(Only for highest tiers, and does NOT bring ANY benefit to you yet. We are just testing data volume.)"),
|
||||
param="EnableSunnylinkUploader"
|
||||
)
|
||||
self._sunnylink_backup_restore_buttons = dual_button_item(
|
||||
description="",
|
||||
left_text=tr("Backup Settings"),
|
||||
right_text=tr("Restore Settings"),
|
||||
left_callback=self._handle_backup_btn,
|
||||
right_callback=self._handle_restore_btn
|
||||
)
|
||||
self._backup_btn: Button = self._sunnylink_backup_restore_buttons.action_item.left_button # store for easy individual access
|
||||
self._restore_btn: Button = self._sunnylink_backup_restore_buttons.action_item.right_button
|
||||
self._backup_btn.set_button_style(ButtonStyle.NORMAL)
|
||||
self._restore_btn.set_button_style(ButtonStyle.PRIMARY)
|
||||
|
||||
items = [
|
||||
SunnylinkHeader(),
|
||||
LineSeparator(),
|
||||
self._sunnylink_toggle,
|
||||
self._sunnylink_description,
|
||||
LineSeparator(),
|
||||
self._sponsor_btn,
|
||||
LineSeparator(),
|
||||
self._pair_btn,
|
||||
LineSeparator(),
|
||||
self._sunnylink_uploader_toggle,
|
||||
LineSeparator(),
|
||||
self._sunnylink_backup_restore_buttons
|
||||
]
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _get_sunnylink_dongle_id() -> str | None:
|
||||
return str(ui_state.params.get("SunnylinkDongleId") or (lambda: tr("N/A")))
|
||||
|
||||
def _handle_pair_btn(self, sponsor_pairing: bool = False):
|
||||
sunnylink_dongle_id = self._get_sunnylink_dongle_id()
|
||||
if sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID:
|
||||
gui_app.set_modal_overlay(alert_dialog(message=tr("sunnylink Dongle ID not found. ") +
|
||||
tr("This may be due to weak internet connection or sunnylink registration issue. ") +
|
||||
tr("Please reboot and try again.")))
|
||||
elif not self._sunnylink_pairing_dialog:
|
||||
self._sunnylink_pairing_dialog = SunnylinkPairingDialog(sponsor_pairing)
|
||||
gui_app.set_modal_overlay(self._sunnylink_pairing_dialog, callback=lambda result: setattr(self, '_sunnylink_pairing_dialog', None))
|
||||
|
||||
def _handle_backup_btn(self):
|
||||
backup_dialog = ConfirmDialog(text=tr("Are you sure you want to backup your current sunnypilot settings?"), confirm_text="Backup")
|
||||
gui_app.set_modal_overlay(backup_dialog, callback=self._backup_handler)
|
||||
|
||||
def _handle_restore_btn(self):
|
||||
self._restore_btn.set_enabled(False)
|
||||
restore_dialog = ConfirmDialog(text=tr("Are you sure you want to restore the last backed up sunnypilot settings?"), confirm_text="Restore")
|
||||
gui_app.set_modal_overlay(restore_dialog, callback=self._restore_handler)
|
||||
|
||||
def _backup_handler(self, dialog_result: int):
|
||||
if dialog_result == DialogResult.CONFIRM:
|
||||
self._backup_in_progress = True
|
||||
self._backup_btn.set_enabled(False)
|
||||
ui_state.params.put_bool("BackupManager_CreateBackup", True)
|
||||
|
||||
def _restore_handler(self, dialog_result: int):
|
||||
if dialog_result == DialogResult.CONFIRM:
|
||||
self._restore_in_progress = True
|
||||
self._restore_btn.set_enabled(False)
|
||||
ui_state.params.put("BackupManager_RestoreVersion", "latest")
|
||||
|
||||
def handle_backup_restore_progress(self):
|
||||
sunnylink_backup_manager = ui_state.sm["backupManagerSP"]
|
||||
|
||||
backup_status = sunnylink_backup_manager.backupStatus
|
||||
restore_status = sunnylink_backup_manager.restoreStatus
|
||||
backup_progress = sunnylink_backup_manager.backupProgress
|
||||
restore_progress = sunnylink_backup_manager.restoreProgress
|
||||
|
||||
if self._backup_in_progress:
|
||||
self._restore_btn.set_enabled(False)
|
||||
self._backup_btn.set_enabled(False)
|
||||
|
||||
if backup_status == custom.BackupManagerSP.Status.inProgress:
|
||||
self._backup_in_progress = True
|
||||
text = tr(f"Backing up {backup_progress}%")
|
||||
self._backup_btn.set_text(text)
|
||||
|
||||
elif backup_status == custom.BackupManagerSP.Status.failed:
|
||||
self._backup_in_progress = False
|
||||
self._backup_btn.set_enabled(not ui_state.is_onroad())
|
||||
self._backup_btn.set_text(tr("Backup Failed"))
|
||||
|
||||
elif (backup_status == custom.BackupManagerSP.Status.completed or
|
||||
(backup_status == custom.BackupManagerSP.Status.idle and backup_progress == 100.0)):
|
||||
self._backup_in_progress = False
|
||||
dialog = alert_dialog(tr("Settings backup completed."))
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
self._backup_btn.set_enabled(not ui_state.is_onroad())
|
||||
|
||||
elif self._restore_in_progress:
|
||||
self._restore_btn.set_enabled(False)
|
||||
self._backup_btn.set_enabled(False)
|
||||
|
||||
if restore_status == custom.BackupManagerSP.Status.inProgress:
|
||||
self._restore_in_progress = True
|
||||
text = tr(f"Restoring {restore_progress}%")
|
||||
self._restore_btn.set_text(text)
|
||||
|
||||
elif restore_status == custom.BackupManagerSP.Status.failed:
|
||||
self._restore_in_progress = False
|
||||
self._restore_btn.set_enabled(not ui_state.is_onroad())
|
||||
self._restore_btn.set_text(tr("Restore Failed"))
|
||||
dialog = alert_dialog(tr("Unable to restore the settings, try again later."))
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
|
||||
elif (restore_status == custom.BackupManagerSP.Status.completed or
|
||||
(restore_status == custom.BackupManagerSP.Status.idle and restore_progress == 100.0)):
|
||||
self._restore_in_progress = False
|
||||
dialog = alert_dialog(tr("Settings restored. Confirm to restart the interface."))
|
||||
gui_app.set_modal_overlay(dialog, callback=lambda: gui_app.request_close())
|
||||
|
||||
else:
|
||||
can_enable = self._sunnylink_enabled and not ui_state.is_onroad()
|
||||
self._backup_btn.set_enabled(can_enable)
|
||||
self._backup_btn.set_text(tr("Backup Settings"))
|
||||
self._restore_btn.set_enabled(can_enable)
|
||||
self._restore_btn.set_text(tr("Restore Settings"))
|
||||
|
||||
def _sunnylink_toggle_callback(self, state: bool):
|
||||
if state:
|
||||
description = tr(
|
||||
"Welcome back!! We're excited to see you've enabled sunnylink again!")
|
||||
color = rl.Color(0, 255, 0, 255) # Green
|
||||
else:
|
||||
description = ("😢 " + tr("Not going to lie, it's sad to see you disabled sunnylink") +
|
||||
tr(", but we'll be here when you're ready to come back."))
|
||||
color = rl.Color(255, 165, 0, 255) # Orange
|
||||
self._sunnylink_description.set_text(description)
|
||||
self._sunnylink_description.set_color(color)
|
||||
self._sunnylink_description.set_visible(True)
|
||||
self._sunnylink_toggle.show_description(False)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._sunnylink_enabled = ui_state.params.get_bool("SunnylinkEnabled")
|
||||
self._sunnylink_toggle.set_right_value(tr("Dongle ID") + ": " + self._get_sunnylink_dongle_id())
|
||||
self._sunnylink_toggle.action_item.set_enabled(not ui_state.is_onroad())
|
||||
self._sunnylink_toggle.action_item.set_state(self._sunnylink_enabled)
|
||||
self._sunnylink_uploader_toggle.action_item.set_enabled(self._sunnylink_enabled)
|
||||
self.handle_backup_restore_progress()
|
||||
|
||||
sponsor_btn_text = tr("THANKS ♥") if ui_state.sunnylink_state.is_sponsor() else tr("SPONSOR")
|
||||
tier_name = ui_state.sunnylink_state.get_sponsor_tier().name.capitalize() or tr("Not Sponsor")
|
||||
self._sponsor_btn.action_item.set_text(sponsor_btn_text)
|
||||
self._sponsor_btn.action_item.set_value(tier_name, ui_state.sunnylink_state.get_sponsor_tier_color())
|
||||
self._sponsor_btn.action_item.set_enabled(self._sunnylink_enabled)
|
||||
|
||||
pair_btn_text = tr("Paired") if ui_state.sunnylink_state.is_paired() else tr("Not Paired")
|
||||
self._pair_btn.action_item.set_text(pair_btn_text)
|
||||
self._pair_btn.action_item.set_enabled(self._sunnylink_enabled)
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
self._sunnylink_description.set_visible(False)
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class TripsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
items = [
|
||||
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import ButtonAction
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.factory import BrandSettingsFactory
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.platform_selector import PlatformSelector, LegendWidget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
|
||||
|
||||
|
||||
class VehicleLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._brand_settings = None
|
||||
self._brand_items = []
|
||||
self._current_brand = None
|
||||
self._platform_selector = PlatformSelector(self._update_brand_settings)
|
||||
|
||||
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("Select")),
|
||||
callback=self._platform_selector._on_clicked)
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
self._legend_widget = LegendWidget(self._platform_selector)
|
||||
|
||||
self.items = [self._vehicle_item, self._legend_widget]
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
@staticmethod
|
||||
def get_brand():
|
||||
if bundle := ui_state.params.get("CarPlatformBundle"):
|
||||
return bundle.get("brand", "")
|
||||
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
|
||||
return ui_state.CP.brand
|
||||
return ""
|
||||
|
||||
def _update_brand_settings(self):
|
||||
self._vehicle_item._title = self._platform_selector.text
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
vehicle_text = tr("Remove") if ui_state.params.get("CarPlatformBundle") else tr("Select")
|
||||
self._vehicle_item.action_item.set_text(vehicle_text)
|
||||
|
||||
brand = self.get_brand()
|
||||
if brand != self._current_brand:
|
||||
self._current_brand = brand
|
||||
self._brand_settings = BrandSettingsFactory.create_brand_settings(brand)
|
||||
self._brand_items = self._brand_settings.items if self._brand_settings else []
|
||||
|
||||
self.items = [self._vehicle_item, self._legend_widget] + self._brand_items
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
def _update_state(self):
|
||||
self._update_brand_settings()
|
||||
if self._brand_settings:
|
||||
self._brand_settings.update_settings()
|
||||
self._platform_selector.refresh()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import abc
|
||||
|
||||
|
||||
class BrandSettings(abc.ABC):
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_settings(self) -> None:
|
||||
"""Update the settings based on the current vehicle brand."""
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class BodySettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class ChryslerSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,45 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.body import BodySettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.chrysler import ChryslerSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.ford import FordSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.gm import GMSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.honda import HondaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.hyundai import HyundaiSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.mazda import MazdaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.nissan import NissanSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.psa import PSASettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.rivian import RivianSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.subaru import SubaruSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.tesla import TeslaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.toyota import ToyotaSettings
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.volkswagen import VolkswagenSettings
|
||||
|
||||
|
||||
class BrandSettingsFactory:
|
||||
_BRAND_MAP: dict[str, type[BrandSettings]] = {
|
||||
"body": BodySettings,
|
||||
"chrysler": ChryslerSettings,
|
||||
"ford": FordSettings,
|
||||
"gm": GMSettings,
|
||||
"honda": HondaSettings,
|
||||
"hyundai": HyundaiSettings,
|
||||
"mazda": MazdaSettings,
|
||||
"nissan": NissanSettings,
|
||||
"psa": PSASettings,
|
||||
"rivian": RivianSettings,
|
||||
"subaru": SubaruSettings,
|
||||
"tesla": TeslaSettings,
|
||||
"toyota": ToyotaSettings,
|
||||
"volkswagen": VolkswagenSettings,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_brand_settings(brand: str) -> BrandSettings | None:
|
||||
cls = BrandSettingsFactory._BRAND_MAP.get(brand)
|
||||
return cls() if cls is not None else None
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class FordSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class GMSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class HondaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
|
||||
from opendbc.car.hyundai.values import CAR, CANFD_UNSUPPORTED_LONGITUDINAL_CAR, UNSUPPORTED_LONGITUDINAL_CAR
|
||||
|
||||
|
||||
class HyundaiSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.alpha_long_available = False
|
||||
|
||||
tuning_texts = [tr("Off"), tr("Dynamic"), tr("Predictive")]
|
||||
self.longitudinal_tuning_item = multiple_button_item_sp(tr("Custom Longitudinal Tuning"), "", tuning_texts,
|
||||
button_width=300, callback=self._on_tuning_selected,
|
||||
param="HyundaiLongitudinalTuning", inline=False)
|
||||
self.items = [self.longitudinal_tuning_item]
|
||||
|
||||
@staticmethod
|
||||
def _on_tuning_selected(index):
|
||||
ui_state.params.put("HyundaiLongitudinalTuning", index)
|
||||
|
||||
def update_settings(self):
|
||||
self.alpha_long_available = False
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
platform = bundle.get("platform")
|
||||
self.alpha_long_available = CAR[platform] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR)
|
||||
elif ui_state.CP:
|
||||
self.alpha_long_available = ui_state.CP.alphaLongitudinalAvailable
|
||||
|
||||
tuning_param = int(ui_state.params.get("HyundaiLongitudinalTuning") or "0")
|
||||
long_enabled = ui_state.has_longitudinal_control
|
||||
|
||||
long_tuning_descs = [
|
||||
tr("Your vehicle will use the Default longitudinal tuning."),
|
||||
tr("Your vehicle will use the Dynamic longitudinal tuning."),
|
||||
tr("Your vehicle will use the Predictive longitudinal tuning."),
|
||||
]
|
||||
long_tuning_desc = long_tuning_descs[tuning_param] if tuning_param < len(long_tuning_descs) else long_tuning_descs[0]
|
||||
|
||||
longitudinal_tuning_disabled = not ui_state.is_offroad() or not long_enabled
|
||||
if longitudinal_tuning_disabled:
|
||||
if not ui_state.is_offroad():
|
||||
long_tuning_desc = tr("This feature is unavailable while the car is onroad.")
|
||||
elif not long_enabled:
|
||||
long_tuning_desc = tr("This feature is unavailable because sunnypilot Longitudinal Control (Alpha) is not enabled.")
|
||||
|
||||
self.longitudinal_tuning_item.action_item.set_enabled(not longitudinal_tuning_disabled)
|
||||
self.longitudinal_tuning_item.set_description(long_tuning_desc)
|
||||
self.longitudinal_tuning_item.show_description(True)
|
||||
self.longitudinal_tuning_item.action_item.set_selected_button(tuning_param)
|
||||
self.longitudinal_tuning_item.set_visible(self.alpha_long_available)
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class MazdaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class NissanSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class PSASettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class RivianSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from opendbc.car.subaru.values import CAR, SubaruFlags
|
||||
|
||||
|
||||
class SubaruSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.has_stop_and_go = False
|
||||
|
||||
self.stop_and_go_toggle = toggle_item_sp(tr("Stop and Go (Beta)"), "", param="SubaruStopAndGo", callback=self._on_toggle_changed)
|
||||
|
||||
self.stop_and_go_manual_parking_brake_toggle = toggle_item_sp(tr("Stop and Go for Manual Parking Brake (Beta)"), "",
|
||||
param="SubaruStopAndGoManualParkingBrake", callback=self._on_toggle_changed)
|
||||
|
||||
self.items = [self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle]
|
||||
|
||||
def _on_toggle_changed(self, _):
|
||||
self.update_settings()
|
||||
|
||||
def stop_and_go_disabled_msg(self):
|
||||
if not self.has_stop_and_go:
|
||||
return tr("This feature is currently not available on this platform.")
|
||||
elif not ui_state.is_offroad():
|
||||
return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
|
||||
return ""
|
||||
|
||||
def update_settings(self):
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
platform = bundle.get("platform")
|
||||
config = CAR[platform].config
|
||||
self.has_stop_and_go = not (config.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
elif ui_state.CP:
|
||||
self.has_stop_and_go = not (ui_state.CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
|
||||
disabled_msg = self.stop_and_go_disabled_msg()
|
||||
descriptions = [
|
||||
tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms."),
|
||||
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!")
|
||||
]
|
||||
|
||||
for toggle, desc in zip([self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle], descriptions, strict=True):
|
||||
toggle.action_item.set_enabled(self.has_stop_and_go and ui_state.is_offroad())
|
||||
toggle.set_description(f"<b>{disabled_msg}</b><br><br>{desc}" if disabled_msg else desc)
|
||||
@@ -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.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
|
||||
COOP_STEERING_MIN_KMH = 23
|
||||
OEM_STEERING_MIN_KMH = 48
|
||||
KM_TO_MILE = 0.621371
|
||||
|
||||
|
||||
class TeslaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
|
||||
self.items = [self.coop_steering_toggle]
|
||||
|
||||
def update_settings(self):
|
||||
is_metric = ui_state.is_metric
|
||||
unit = "km/h" if is_metric else "mph"
|
||||
|
||||
display_value_coop = COOP_STEERING_MIN_KMH if is_metric else round(COOP_STEERING_MIN_KMH * KM_TO_MILE)
|
||||
display_value_oem = OEM_STEERING_MIN_KMH if is_metric else round(OEM_STEERING_MIN_KMH * KM_TO_MILE)
|
||||
|
||||
coop_steering_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
|
||||
coop_steering_warning = tr(f"Warning: May experience steering oscillations below {display_value_oem} {unit} during turns, " +
|
||||
"recommend disabling this feature if you experience these.")
|
||||
coop_steering_desc = (
|
||||
f"<b>{coop_steering_warning}</b><br><br>" +
|
||||
f"{tr('Allows the driver to provide limited steering input while openpilot is engaged.')}<br>" +
|
||||
f"{tr(f'Only works above {display_value_coop} {unit}.')}"
|
||||
)
|
||||
|
||||
if not ui_state.is_offroad():
|
||||
coop_steering_desc = f"<b>{coop_steering_disabled_msg}</b><br><br>{coop_steering_desc}"
|
||||
|
||||
self.coop_steering_toggle.set_description(coop_steering_desc)
|
||||
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class ToyotaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
|
||||
|
||||
class VolkswagenSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user