Compare commits

...

20 Commits

Author SHA1 Message Date
Jason Wen 87af129090 tici: fix staging root updates (#1224) 2025-09-06 18:46:19 -04:00
Jason Wen f6cf1336ef update: actually detect device type as TICI (#1222) 2025-09-06 18:08:26 -04:00
Jason Wen 51046dd7fd ci: restore cache based on master-tici 2025-09-06 17:43:36 -04:00
Jason Wen 2c85f29a0b staging-c3-new > staging-tici 2025-09-06 17:38:11 -04:00
Jason Wen 68af0c4a17 ci: update for master-tici (#1220)
* ci: master > master-ci

* disable reset and squash for master-tici

* disable release drafter

* docker img

* temp builds

* actual temp

* new ref

* Revert "actual temp"

This reverts commit 2e73befd17.

* wrong name

* Revert "wrong name"

This reverts commit 5efa687aaf.

* Reapply "actual temp"

This reverts commit 75a9d986fd.

* Revert "Reapply "actual temp""

This reverts commit 1f77d902d4.

* Reapply "wrong name"

This reverts commit ed24def13c.

* revert
2025-09-06 17:28:43 -04:00
Jason Wen b161764b1e update: sunnypilot branch migrations for tici (#1212)
* update: sunnypilot branch migrations for tici

* block onroad and notify

* type

* check channel type

* update

* ui init

* no search and locked list for tici

* whenever available
2025-09-06 15:26:32 -04:00
Jason Wen 7057c57419 ui: cleanup cereal event params parsing (#1219)
* Revert "bugfix: streamline LiveDelay parameter loading with safe handling (#1204)"

This reverts commit 288a5e14da.

* ui: use AlignedBuffer for cereal data processing for Models panel

* align

* separate

* split

* event it

* no more backup

* Revert "no more backup"

This reverts commit fa66ce5e77.
2025-09-05 20:51:58 -04:00
DevTekVE 7d4df73ea5 hotfix: model fetcher warning instead of exception when fetching fail 2025-09-05 14:42:08 +02:00
James Vecellio-Grant 29fe152bd3 modeld_v2: desire rename and add many parts from thneed modeld (#1197)
* Add model metadata lookup and update desire handling

* Bump selector version to 10

* meh

* Refactor shape mode parameters for desire handling in test buffer logic

* loop more models

* Refactor buffer handling for temporal inputs and streamline desire updates

* Refactor lateral control input handling and remove unused code
2025-09-04 22:26:59 -04:00
Jason Wen 31918c067a events: add sunnypilot/openpilot to remote origin check (#1216)
events: add sunnypilot/openpilot to remote origin check
2025-09-04 22:22:48 -04:00
Jason Wen daf5ea2783 update: remove git cleanup in finalized stage (#1210)
* updater: remove git cleanup in finalized stage for quicker updates

* nah
2025-09-04 22:10:08 -04:00
github-actions[bot] fd7295c980 [bot] Update Python packages (#1214)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-09-04 14:31:01 -04:00
Jason Wen 220cfff04d ci: skip uv lock upgrade on forks (#1213) 2025-09-04 14:29:11 -04:00
Jason Wen ee0fb6bf8e Revert "[bot] Update Python packages" (#1211)
Revert "[bot] Update Python packages (#1201)"

This reverts commit 0cd2bbf6c0.
2025-09-04 14:19:31 -04:00
github-actions[bot] 0cd2bbf6c0 [bot] Update Python packages (#1201)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-09-04 10:08:51 -04:00
DevTekVE 0871abcf55 bugfix: fix fetching params for sunnylink and backup (#1177)
* Hotfix for the params stuff until I rework this properly and leverage the new data types

* Revert "Hotfix for the params stuff until I rework this properly and leverage the new data types"

This reverts commit c6031b29d92d3ff5b679ffce3ba53611bb2dba0e.

* refactor: enhance getParams function to support JSON and bytes types with optional compression

* refactor: add TODO for enhancing server support of metadata in sunnylinkd.py

* lint and clean

* refactor: update value handling in getParams to return decoded values for JSON serialization

* refactor: simplify params_dict initialization by removing type hint

* refactor: update response handling in getParams to include JSON serialization of params

* refactor: update response handling in getParams to include JSON serialization of params

* Add to dic types

* refactor: extract get_param_as_byte function for improved parameter handling and fix backup inconsistencies

* cleanup

* ensure error propagates on backup fail
2025-09-04 14:45:37 +02:00
DevTekVE 8ccb777192 bugfix: improve exception handling for sunnylinkd (SUN-89) (#1207)
* bugfix: improve exception handling for WebSocket connections in sunnylinkd

* bugfix: enhance exception handling for WebSocket connections in sunnylinkd

* bugfix: improve OSError handling in sunnylinkd for better error reporting
2025-09-04 14:45:03 +02:00
DevTekVE 0593667601 bugfix: improve error handling in model fetching process (SUN-87) (#1205)
* bugfix: improve error handling in model fetching process

* cleanup

* bugfix: refine error handling in model fetching process
2025-09-04 14:44:42 +02:00
DevTekVE 288a5e14da bugfix: streamline LiveDelay parameter loading with safe handling (#1204) 2025-09-03 17:18:18 +02:00
James Vecellio-Grant 9447aa0e3d modeld: turn desires (#1182)
* Add modelDataV2SP and lane turn logic implementation

Note: still need to hook up to other modeld's create unit test, fix stuff, and do the UI for it

* add unit tests for lane turn logic

* Add lane turn desire controls to models panel

* use `events_sp` instead of `events`

* integrate modelDataV2SP messaging to the other modeld controllers

* move this to that

* use min for general population here, on custom branches, change this to max :)

* Update events.py

Co-authored-by: royjr <royjr96@gmail.com>

* Update events.py

Co-authored-by: royjr <royjr96@gmail.com>

* refactor lane turn value control into one method

* Update selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc

* add integration tests for lane turn desire

* 10 updates is possibly more representative of real life

* real objects ofc

* desc: add toggle description for clarity

---------

Co-authored-by: royjr <royjr96@gmail.com>
2025-09-03 05:49:12 -07:00
49 changed files with 614 additions and 247 deletions
+2 -2
View File
@@ -29,11 +29,11 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target: /^(?!master$).*/
target: /^(?!master-tici$).*/
exclude: /sunnypilot:.*/
change-to: ${{ github.base_ref }}
already-exists-action: close_this
already-exists-comment: "Your PR should be made against the `master` branch"
already-exists-comment: "Your PR should be made against the `master-tici` branch"
update-pr-labels:
name: Update fork's PR Labels
+1 -1
View File
@@ -5,7 +5,7 @@ on:
workflow_dispatch:
env:
BASE_IMAGE: sunnypilot-base
BASE_IMAGE: sunnypilot-tici-base
DOCKER_REGISTRY: ghcr.io/sunnypilot
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -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 $DOCKER_REGISTRY/$BASE_IMAGE:latest /bin/bash -c
+2 -2
View File
@@ -3,7 +3,7 @@ name: cereal validation
on:
push:
branches:
- master
- master-tici
pull_request:
paths:
- 'cereal/**'
@@ -16,7 +16,7 @@ on:
type: string
concurrency:
group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
strategy:
fail-fast: false
matrix: ${{fromJSON(needs.setup.outputs.ci_runs)}}
uses: sunnypilot/sunnypilot/.github/workflows/ci_weekly_run.yaml@master
uses: sunnypilot/sunnypilot/.github/workflows/ci_weekly_run.yaml@master-tici
with:
run_number: ${{ matrix.run_number }}
+1 -1
View File
@@ -12,6 +12,6 @@ concurrency:
jobs:
selfdrive_tests:
uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master
uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master-tici
with:
run_number: ${{ inputs.run_number }}
@@ -15,7 +15,7 @@ runs:
scons -j$(nproc) --cache-populate"
- name: Save scons cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master-tici'
with:
path: .ci_cache/scons_cache
key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
+4 -4
View File
@@ -3,7 +3,7 @@ name: docs
on:
push:
branches:
- master
- master-tici
pull_request:
workflow_call:
inputs:
@@ -12,7 +12,7 @@ on:
required: true
type: string
concurrency:
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
@@ -35,13 +35,13 @@ jobs:
# Push to docs.comma.ai
- uses: actions/checkout@v4
if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot'
if: github.ref == 'refs/heads/master-tici' && github.repository == 'sunnypilot/sunnypilot'
with:
path: openpilot-docs
ssh-key: ${{ secrets.OPENPILOT_DOCS_KEY }}
repository: sunnypilot/sunnypilot-docs
- name: Push
if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot'
if: github.ref == 'refs/heads/master-tici' && github.repository == 'sunnypilot/sunnypilot'
run: |
set -x
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master
ref: master-tici
wait-interval: 30
running-workflow-name: 'build prebuilt'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build master-ci).*).)*$
check-regexp: ^((?!.*(build __nightly).*).)*$
- uses: actions/checkout@v4
with:
submodules: true
+1
View File
@@ -19,6 +19,7 @@ jobs:
contents: write
pull-requests: write
runs-on: ubuntu-latest
if: False
steps:
- uses: release-drafter/release-drafter@v6
with:
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
env:
ImageOS: ubuntu24
container:
image: ghcr.io/sunnypilot/sunnypilot-base:latest
image: ghcr.io/sunnypilot/sunnypilot-tici-base:latest
runs-on: ubuntu-latest
if: github.repository == 'sunnypilot/sunnypilot'
permissions:
@@ -25,7 +25,7 @@ jobs:
if: ${{ github.event_name == 'schedule' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master
ref: master-tici
wait-interval: 30
running-workflow-name: 'build __nightly'
repo-token: ${{ secrets.GITHUB_TOKEN }}
+5 -4
View File
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
env:
BASE_IMAGE: sunnypilot-base
BASE_IMAGE: sunnypilot-tici-base
BUILD: release/ci/docker_build_sp.sh base
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
@@ -28,7 +28,7 @@ jobs:
title: "[bot] Update translations"
body: "Automatic PR from repo-maintenance -> update_translations"
branch: "update-translations"
base: "master"
base: "master-tici"
delete-branch: true
labels: bot
@@ -36,13 +36,14 @@ jobs:
name: package_updates
runs-on: ubuntu-latest
container:
image: ghcr.io/sunnypilot/sunnypilot-base:latest
image: ghcr.io/sunnypilot/sunnypilot-tici-base:latest
if: github.repository == 'sunnypilot/sunnypilot'
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: uv lock
if: github.repository == 'commaai/openpilot'
run: |
python3 -m ensurepip --upgrade
pip3 install uv
@@ -67,7 +68,7 @@ jobs:
commit-message: Update Python packages
title: '[bot] Update Python packages'
branch: auto-package-updates
base: master
base: master-tici
delete-branch: true
body: 'Automatic PR from repo-maintenance -> package_updates'
labels: bot
+10 -10
View File
@@ -3,7 +3,7 @@ name: selfdrive
on:
push:
branches:
- master
- master-tici
pull_request:
workflow_dispatch:
workflow_call:
@@ -14,12 +14,12 @@ on:
type: string
concurrency:
group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:
PYTHONWARNINGS: error
BASE_IMAGE: sunnypilot-base
BASE_IMAGE: sunnypilot-tici-base
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
@@ -68,10 +68,10 @@ jobs:
if: github.repository == 'sunnypilot/sunnypilot'
timeout-minutes: 3
run: |
if [ "${{ github.ref }}" != "refs/heads/master" ]; then
git fetch origin master:refs/remotes/origin/master
if [ "${{ github.ref }}" != "refs/heads/master-tici" ]; then
git fetch origin master-tici:refs/remotes/origin/master-tici
SUBMODULE_PATHS=$(git diff origin/master HEAD --name-only | grep -E '^[^/]+$' | while read path; do
SUBMODULE_PATHS=$(git diff origin/master-tici HEAD --name-only | grep -E '^[^/]+$' | while read path; do
if git ls-files --stage "$path" | grep -q "^160000"; then
echo "$path"
fi
@@ -97,7 +97,7 @@ jobs:
with:
submodules: true
- name: Setup docker push
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'sunnypilot/sunnypilot'
if: github.ref == 'refs/heads/master-tici' && github.event_name != 'pull_request' && github.repository == 'sunnypilot/sunnypilot'
run: |
echo "PUSH_IMAGE=true" >> "$GITHUB_ENV"
$DOCKER_LOGIN
@@ -129,7 +129,7 @@ jobs:
PYTHONWARNINGS: default
- name: Save Homebrew cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master-tici'
with:
path: ~/Library/Caches/Homebrew
key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
@@ -146,7 +146,7 @@ jobs:
run: . .venv/bin/activate && scons -j$(nproc)
- name: Save scons cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master-tici'
with:
path: /tmp/scons_cache
key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
@@ -305,5 +305,5 @@ jobs:
- name: Upload Test Report
uses: actions/upload-artifact@v4
with:
name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && 'master-tici' || github.event.number }}
path: selfdrive/ui/tests/test_ui/report_1/screenshots
@@ -14,7 +14,7 @@ on:
upstream_branch:
description: 'Upstream branch to build from'
required: true
default: 'master'
default: 'master-tici'
type: string
custom_name:
description: 'Custom name for the model (no date, only name)'
@@ -35,7 +35,7 @@ on:
upstream_branch:
description: 'Upstream branch to build from'
required: true
default: 'master'
default: 'master-tici'
type: string
custom_name:
description: 'Custom name for the model (no date, only name)'
@@ -8,14 +8,14 @@ env:
PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot"
# Branch configurations
STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master' }} # vars are set on repo settings.
STAGING_C3_SOURCE_BRANCH: 'master-tici' # vars.STAGING_C3_SOURCE_BRANCH could be used, set on repo settings.
# Runtime configuration
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
on:
push:
branches: [ master, master-dev-c3-new ]
branches: [ master-tici ]
tags: [ 'release/*' ]
pull_request_target:
types: [ labeled ]
@@ -1,7 +1,7 @@
name: Build dev-c3-new
env:
DEFAULT_SOURCE_BRANCH: "master"
DEFAULT_SOURCE_BRANCH: "master-tici"
DEFAULT_TARGET_BRANCH: "master-dev-c3-new"
PR_LABEL: "dev-c3"
LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs'
@@ -10,17 +10,17 @@ env:
on:
push:
branches:
- master
- master-tici
pull_request_target:
types: [ labeled ]
branches:
- 'master'
- 'master-tici'
workflow_dispatch:
inputs:
source_branch:
description: 'Source branch to reset from'
required: true
default: 'master'
default: 'master-tici'
type: string
target_branch:
description: 'Target branch to reset and squash into'
@@ -40,11 +40,7 @@ concurrency:
jobs:
reset-and-squash:
runs-on: ubuntu-latest
if: (
(github.event_name == 'workflow_dispatch')
|| (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch))
|| (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3'))))
)
if: False
steps:
- uses: actions/checkout@v4
with:
+10 -10
View File
@@ -2,19 +2,19 @@ name: "ui preview"
on:
push:
branches:
- master
- master-tici
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]
branches:
- 'master'
- 'master-tici'
paths:
- 'selfdrive/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create UI Report"
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && 'master-tici' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.sha || github.event.pull_request.head.sha }}
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}"
jobs:
@@ -55,7 +55,7 @@ jobs:
name: report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master ui
- name: Getting master-tici ui
uses: actions/checkout@v4
with:
repository: sunnypilot/ci-artifacts
@@ -63,8 +63,8 @@ jobs:
path: ${{ github.workspace }}/master_ui
ref: openpilot_master_ui
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
- name: Saving new master-tici ui
if: github.ref == 'refs/heads/master-tici' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui
run: |
git checkout --orphan=new_master_ui
@@ -93,9 +93,9 @@ jobs:
for ((i=0; i<${#A[*]}; i=i+1));
do
# Check if the master file exists
# Check if the master-tici file exists
if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then
# This is a new file in PR UI that doesn't exist in master
# This is a new file in PR UI that doesn't exist in master-tici
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
DIFF="${DIFF}<table>"
@@ -118,7 +118,7 @@ jobs:
DIFF="${DIFF}<table>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> master <img src=\"https://raw.githubusercontent.com/sunnypilot/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>"
DIFF="${DIFF} <td> master-tici <img src=\"https://raw.githubusercontent.com/sunnypilot/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>"
DIFF="${DIFF} <td> proposed <img src=\"https://raw.githubusercontent.com/sunnypilot/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
DIFF="${DIFF}</tr>"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ghcr.io/sunnypilot/sunnypilot-base:latest
FROM ghcr.io/sunnypilot/sunnypilot-tici-base:latest
ENV PYTHONUNBUFFERED=1
+9 -10
View File
@@ -22,7 +22,7 @@ https://docs.sunnypilot.ai/ is your one stop shop for everything from features t
Detailed instructions for [how to mount the device in a car](https://comma.ai/setup).
## Installation
Please refer to [Recommended Branches](#-recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging-c3-new` branch.
Please refer to [Recommended Branches](#-recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging-tici` branch.
### If you want to use our newest branches (our rewrite)
> [!TIP]
@@ -31,25 +31,24 @@ Please refer to [Recommended Branches](#-recommended-branches) to find your pref
* sunnypilot not installed or you installed a version before 0.8.17?
1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed.
2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option.
3. Input the installation URL per [Recommended Branches](#-recommended-branches). Example: ```https://staging-c3-new.sunnypilot.ai```.
3. Input the installation URL per [Recommended Branches](#-recommended-branches). Example: ```https://staging-tici.sunnypilot.ai```.
4. Complete the rest of the installation following the onscreen instructions.
* sunnypilot already installed and you installed a version after 0.8.17?
1. On the comma three, go to `Settings` ▶️ `Software`.
2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot.
3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector.
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging-c3-new`
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging-tici`
| Branch | Installation URL |
|:----------------:|:---------------------------------------------:|
| `staging-c3-new` | `https://staging-c3-new.sunnypilot.ai` |
| `dev-c3-new` | `https://dev-c3-new.sunnypilot.ai` |
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
| `release-c3-new` | **Not yet available**. |
| Branch | Installation URL |
|:---------------:|:---------------------------------------------:|
| `staging-tici` | `https://staging-tici.sunnypilot.ai` |
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
| `release-tici` | **Not yet available**. |
> [!TIP]
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging-c3-new'.
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging-tici'.
> [!NOTE]
> Do you require further assistance with software installation? Join the [sunnypilot Discord server](https://discord.sunnypilot.com) and message us in the `#installation-help` channel.
+10 -1
View File
@@ -172,6 +172,8 @@ struct OnroadEventSP @0xda96579883444c35 {
experimentalModeSwitched @14;
wrongCarModeAlertOnly @15;
pedalPressedAlertOnly @16;
laneTurnLeft @17;
laneTurnRight @18;
}
}
@@ -258,9 +260,16 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
roadName @5 :Text;
}
struct CustomReserved9 @0xa1680744031fdb2d {
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
}
enum TurnDirection {
none @0;
turnLeft @1;
turnRight @2;
}
struct CustomReserved10 @0xcb9fd56c7057593a {
}
+1 -1
View File
@@ -2631,7 +2631,7 @@ struct Event {
backupManagerSP @113 :Custom.BackupManagerSP;
carStateSP @114 :Custom.CarStateSP;
liveMapDataSP @115 :Custom.LiveMapDataSP;
customReserved9 @116 :Custom.CustomReserved9;
modelDataV2SP @116 :Custom.ModelDataV2SP;
customReserved10 @136 :Custom.CustomReserved10;
customReserved11 @137 :Custom.CustomReserved11;
customReserved12 @138 :Custom.CustomReserved12;
+1
View File
@@ -88,6 +88,7 @@ _services: dict[str, tuple] = {
"carControlSP": (True, 100., 10),
"carStateSP": (True, 100., 10),
"liveMapDataSP": (True, 1., 1),
"modelDataV2SP": (True, 20.),
# debug
"uiDebug": (True, 0., 1),
+4 -1
View File
@@ -154,6 +154,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
{"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -195,10 +196,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
// model panel params
// sunnypilot model params
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
// mapd
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
+1 -1
Submodule panda updated: f10ddc6a89...7eab6fd61b
+2 -2
View File
@@ -1,8 +1,8 @@
if [ "$1" = "base" ]; then
export DOCKER_IMAGE=sunnypilot-base
export DOCKER_IMAGE=sunnypilot-tici-base
export DOCKER_FILE=Dockerfile.sunnypilot_base
elif [ "$1" = "prebuilt" ]; then
export DOCKER_IMAGE=sunnypilot-prebuilt
export DOCKER_IMAGE=sunnypilot-tici-prebuilt
export DOCKER_FILE=Dockerfile.sunnypilot
else
echo "Invalid docker build image: '$1'"
+20 -2
View File
@@ -1,7 +1,8 @@
from cereal import log
from cereal import log, custom
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController
LaneChangeState = log.LaneChangeState
LaneChangeDirection = log.LaneChangeDirection
@@ -30,6 +31,12 @@ DESIRES = {
},
}
TURN_DESIRES = {
custom.TurnDirection.none: log.Desire.none,
custom.TurnDirection.turnLeft: log.Desire.turnLeft,
custom.TurnDirection.turnRight: log.Desire.turnRight,
}
class DesireHelper:
def __init__(self):
@@ -41,13 +48,21 @@ class DesireHelper:
self.prev_one_blinker = False
self.desire = log.Desire.none
self.alc = AutoLaneChangeController(self)
self.lane_turn_controller = LaneTurnController(self)
self.lane_turn_direction = custom.TurnDirection.none
def update(self, carstate, lateral_active, lane_change_prob):
self.alc.update_params()
self.lane_turn_controller.update_params()
v_ego = carstate.vEgo
one_blinker = carstate.leftBlinker != carstate.rightBlinker
below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN
# Lane turn controller update
self.lane_turn_controller.update_lane_turn(blindspot_left=carstate.leftBlindspot, blindspot_right=carstate.rightBlindspot,
left_blinker=carstate.leftBlinker, right_blinker=carstate.rightBlinker, v_ego=v_ego)
self.lane_turn_direction = self.lane_turn_controller.get_turn_direction()
if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF:
self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none
@@ -106,7 +121,10 @@ class DesireHelper:
self.prev_one_blinker = one_blinker
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
if self.lane_turn_direction != custom.TurnDirection.none:
self.desire = TURN_DESIRES[self.lane_turn_direction]
else:
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
# Send keep pulse once per second during LaneChangeStart.preLaneChange
if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting):
+4 -1
View File
@@ -216,7 +216,7 @@ def main(demo=False):
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
# messaging
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
publish_state = PublishState()
@@ -333,6 +333,7 @@ def main(demo=False):
modelv2_send = messaging.new_message('modelV2')
drivingdata_send = messaging.new_message('drivingModelData')
posenet_send = messaging.new_message('cameraOdometry')
mdv2sp_send = messaging.new_message('modelDataV2SP')
action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
prev_action = action
@@ -347,6 +348,7 @@ def main(demo=False):
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
@@ -354,6 +356,7 @@ def main(demo=False):
pm.send('modelV2', modelv2_send)
pm.send('drivingModelData', drivingdata_send)
pm.send('cameraOdometry', posenet_send)
pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id
+5
View File
@@ -49,5 +49,10 @@
"text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
"severity": 1,
"_comment": "Set extra field to lateral or longitudinal."
},
"Offroad_TiciSupport": {
"text": "<b>Unsupported branch detected</b> - The current version of <b><u>%1</u></b> branch is no longer supported on the comma three. Please go to <b>[Device > Software]</b> and install a supported branch with <b><u>-tici</u></b> in the branch name for the comma three.",
"severity": 1,
"_comment": "Set extra field to the current branch name."
}
}
+10 -2
View File
@@ -86,7 +86,7 @@ class SelfdriveD(CruiseHelper):
# TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches
self.car_state_sock = messaging.sub_sock('carState', timeout=20)
ignore = self.sensor_packets + self.gps_packets + ['alertDebug']
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP']
if SIMULATION:
ignore += ['driverCameraState', 'managerState']
if REPLAY:
@@ -95,7 +95,8 @@ class SelfdriveD(CruiseHelper):
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback'] + \
'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback',
'modelDataV2SP'] + \
self.camera_packets + self.sensor_packets + self.gps_packets,
ignore_alive=ignore, ignore_avg_freq=ignore,
ignore_valid=ignore, frequency=int(1/DT_CTRL))
@@ -300,6 +301,13 @@ class SelfdriveD(CruiseHelper):
LaneChangeState.laneChangeFinishing):
self.events.add(EventName.laneChange)
# Handle lane turn
lane_turn_direction = self.sm['modelDataV2SP'].laneTurnDirection
if lane_turn_direction == custom.TurnDirection.turnLeft:
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnLeft)
elif lane_turn_direction == custom.TurnDirection.turnRight:
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnRight)
for i, pandaState in enumerate(self.sm['pandaStates']):
# All pandas must match the list of safetyConfigs, and if outside this list, must be silent or noOutput
if i < len(self.CP.safetyConfigs):
@@ -101,32 +101,42 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
QString policyType = tr("Policy Model");
policyFrame = createModelDetailFrame(this, policyType, policyProgressBar);
list->addItem(policyFrame);
list->addItem(horizontal_line());
// Lane Turn Desire toggle
lane_turn_desire_toggle = new ParamControlSP("LaneTurnDesire", tr("Use Lane Turn Desires"),
"If youre driving at 20 mph (32 km/h) or below and have your blinker on, "
"the car will plan a turn in that direction at the nearest drivable path. "
"This prevents situations (like at red lights) where the car might plan the wrong turn direction.",
"../assets/offroad/icon_shell.png");
list->addItem(lane_turn_desire_toggle);
// Lane Turn Value control
int max_value_mph = 20;
bool is_metric_initial = params.getBool("IsMetric");
const float K = 1.609344f;
int per_value_change_scaled = is_metric_initial ? static_cast<int>(std::round((1.0f / K) * 100.0f)) : 100; // 100 -> 1 mph
lane_turn_value_control = new OptionControlSP("LaneTurnValue", tr("Adjust Lane Turn Speed"),
tr("Set the maximum speed for lane turn desires. Default is 19 %1.").arg(is_metric_initial ? "km/h" : "mph"),
"", {5 * 100, max_value_mph * 100}, per_value_change_scaled, false, nullptr, true, true);
lane_turn_value_control->showDescription();
list->addItem(lane_turn_value_control);
// Show based on toggle
refreshLaneTurnValueControl();
connect(lane_turn_desire_toggle, &ParamControlSP::toggleFlipped, this, &ModelsPanel::refreshLaneTurnValueControl);
connect(lane_turn_value_control, &OptionControlSP::updateLabels, this, &ModelsPanel::refreshLaneTurnValueControl);
// LiveDelay toggle
lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png");
lagd_toggle_control->showDescription();
list->addItem(lagd_toggle_control);
// Software delay control
int liveDelayMaxInt = 30;
std::string liveDelayBytes = params.get("LiveDelay");
if (!liveDelayBytes.empty()) {
capnp::FlatArrayMessageReader msg(kj::ArrayPtr<const capnp::word>(
reinterpret_cast<const capnp::word*>(liveDelayBytes.data()),
liveDelayBytes.size() / sizeof(capnp::word)));
auto event = msg.getRoot<cereal::Event>();
if (event.hasLiveDelay()) {
auto liveDelay = event.getLiveDelay();
float lateralDelay = liveDelay.getLateralDelay();
liveDelayMaxInt = static_cast<int>(lateralDelay * 100.0f) + 20;
}
}
delay_control = new OptionControlSP("LagdToggleDelay", tr("Adjust Software Delay"),
tr("Adjust the software delay when Live Learning Steer Delay is toggled off."
"\nThe default software delay value is 0.2"),
"", {5, liveDelayMaxInt}, 1, false, nullptr, true, true);
tr("Adjust the software delay when Live Learning Steer Delay is toggled off."
"\nThe default software delay value is 0.2"),
"", {5, 50}, 1, false, nullptr, true, true);
connect(delay_control, &OptionControlSP::updateLabels, [=]() {
float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat();
@@ -159,6 +169,19 @@ QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName,
return frame;
}
void ModelsPanel::refreshLaneTurnValueControl() {
if (!lane_turn_value_control) return;
float stored_mph = QString::fromStdString(params.get("LaneTurnValue")).toFloat();
bool is_metric = params.getBool("IsMetric");
QString unit = is_metric ? "km/h" : "mph";
float display_value = stored_mph;
if (is_metric) {
display_value = stored_mph * 1.609344f;
}
lane_turn_value_control->setLabel(QString::number(static_cast<int>(std::round(display_value))) + " " + unit);
lane_turn_value_control->setVisible(params.getBool("LaneTurnDesire"));
}
/**
* @brief Updates the UI with bundle download progress information
* Reads status from modelManagerSP cereal message and displays status for all models
@@ -401,34 +424,28 @@ void ModelsPanel::updateLabels() {
"Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.");
bool lagdEnabled = params.getBool("LagdToggle");
if (lagdEnabled) {
std::string liveDelayBytes = params.get("LiveDelay");
auto liveDelayBytes = params.get("LiveDelay");
if (!liveDelayBytes.empty()) {
capnp::FlatArrayMessageReader msg(kj::ArrayPtr<const capnp::word>(
reinterpret_cast<const capnp::word*>(liveDelayBytes.data()),
liveDelayBytes.size() / sizeof(capnp::word)));
auto event = msg.getRoot<cereal::Event>();
if (event.hasLiveDelay()) {
auto liveDelay = event.getLiveDelay();
float lateralDelay = liveDelay.getLateralDelay();
desc += QString("<br><br><b><span style=\"color:#e0e0e0\">%1</span></b> <span style=\"color:#e0e0e0\">%2 s</span>")
.arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3));
}
auto LD = loadCerealEvent(params, "LiveDelay");
float lateralDelay = LD->getLiveDelay().getLateralDelay();
desc += QString("<br><br><b><span style=\"color:#e0e0e0\">%1</span></b> <span style=\"color:#e0e0e0\">%2 s</span>")
.arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3));
}
} else {
std::string carParamsBytes = params.get("CarParamsPersistent");
auto carParamsBytes = params.get("CarParamsPersistent");
if (!carParamsBytes.empty()) {
capnp::FlatArrayMessageReader msg(kj::ArrayPtr<const capnp::word>(
reinterpret_cast<const capnp::word*>(carParamsBytes.data()),
carParamsBytes.size() / sizeof(capnp::word)));
auto carParams = msg.getRoot<cereal::CarParams>();
float steerDelay = carParams.getSteerActuatorDelay();
AlignedBuffer aligned_buf_cp;
capnp::FlatArrayMessageReader cmsg(aligned_buf_cp.align(carParamsBytes.data(), carParamsBytes.size()));
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
float steerDelay = CP.getSteerActuatorDelay();
float softwareDelay = QString::fromStdString(params.get("LagdToggleDelay")).toFloat();
float totalLag = steerDelay + softwareDelay;
desc += QString("<br><br><span style=\"color:#e0e0e0\">"
"<b>%1</b> %2 s + <b>%3</b> %4 s = <b>%5</b> %6 s</span>")
.arg(tr("Actuator Delay:"), QString::number(steerDelay, 'f', 2),
tr("Software Delay:"), QString::number(softwareDelay, 'f', 2),
tr("Total Delay:"), QString::number(totalLag, 'f', 2));
.arg(tr("Actuator Delay:"), QString::number(steerDelay, 'f', 2),
tr("Software Delay:"), QString::number(softwareDelay, 'f', 2),
tr("Total Delay:"), QString::number(totalLag, 'f', 2));
}
}
lagd_toggle_control->setDescription(desc);
@@ -439,6 +456,9 @@ void ModelsPanel::updateLabels() {
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
}
// Update lane turn desire label and visibility
refreshLaneTurnValueControl();
clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB");
}
@@ -9,6 +9,7 @@
#include <QProgressBar>
#include "selfdrive/ui/sunnypilot/qt/util.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
class ModelsPanel : public QWidget {
@@ -37,6 +38,7 @@ private:
void updateLabels();
void handleCurrentModelLblBtnClicked();
void handleBundleDownloadProgress();
void refreshLaneTurnValueControl();
void showResetParamsDialog();
QProgressBar* createProgressBar(QWidget *parent);
QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar);
@@ -81,5 +83,6 @@ private:
Params params;
ButtonControlSP *clearModelCacheBtn;
ButtonControlSP *refreshAvailableModelsBtn;
ParamControlSP *lane_turn_desire_toggle;
OptionControlSP *lane_turn_value_control;
};
@@ -11,12 +11,39 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) {
// branch selector
QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr);
connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() {
InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false);
if (Hardware::get_device_type() == cereal::InitData::DeviceType::TICI) {
auto current = params.get("GitBranch");
QStringList allBranches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(",");
QStringList branches;
for (const QString &b : allBranches) {
if (b.endsWith("-tici")) {
branches.append(b);
}
}
for (QString b : {current.c_str(), "master-tici", "staging-tici", "release-tici"}) {
auto i = branches.indexOf(b);
if (i >= 0) {
branches.removeAt(i);
branches.insert(0, b);
}
}
QString cur = QString::fromStdString(params.get("UpdaterTargetBranch"));
QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this);
if (!selection.isEmpty()) {
params.put("UpdaterTargetBranch", selection.toStdString());
targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch")));
checkForUpdates();
}
} else {
InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false);
d.setMinLength(0);
const int ret = d.exec();
if (ret) {
searchBranches(d.text());
}
}
});
// Disable Updates toggle
+13
View File
@@ -110,3 +110,16 @@ QStringList searchFromList(const QString &query, const QStringList &list) {
}
return search_results;
}
std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::string& _param) {
std::string bytes = params.get(_param);
try {
AlignedBuffer aligned_buf;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(bytes.data(), bytes.size()));
return cmsg.getRoot<cereal::Event>();
} catch (kj::Exception& e) {
qInfo() << "invalid " << QString::fromStdString(_param) << ":" << e.getDescription().cStr();
return std::nullopt;
}
}
+3
View File
@@ -15,8 +15,11 @@
#include <QRegularExpression>
#include <QWidget>
#include "selfdrive/ui/sunnypilot/ui.h"
QString getUserAgent(bool sunnylink = false);
std::optional<QString> getSunnylinkDongleId();
std::optional<QString> getParamIgnoringDefault(const std::string &param_name, const std::string &default_value);
QMap<QString, QVariantMap> loadPlatformList();
QStringList searchFromList(const QString &query, const QStringList &list);
std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::string& _param);
+4 -1
View File
@@ -177,7 +177,7 @@ def main(demo=False):
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
# messaging
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
publish_state = PublishState()
@@ -304,6 +304,7 @@ def main(demo=False):
modelv2_send = messaging.new_message('modelV2')
drivingdata_send = messaging.new_message('drivingModelData')
posenet_send = messaging.new_message('cameraOdometry')
mdv2sp_send = messaging.new_message('modelDataV2SP')
action = model.get_action_from_model(model_output, prev_action, long_delay + DT_MDL)
fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen,
@@ -316,6 +317,7 @@ def main(demo=False):
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
@@ -323,6 +325,7 @@ def main(demo=False):
pm.send('modelV2', modelv2_send)
pm.send('drivingModelData', drivingdata_send)
pm.send('cameraOdometry', posenet_send)
pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id
+31 -22
View File
@@ -27,7 +27,7 @@ from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
PROCESS_NAME = "selfdrive.modeld.modeld"
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
class FrameMeta:
@@ -77,42 +77,47 @@ class ModelState(ModelStateBase):
self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32)
# Temporal input: shape is [batch, history, features]
if len(shape) == 3 and shape[1] > 1:
buffer_history_len = max(100, (shape[1] * 4 if shape[1] < 100 else shape[1])) # Allow for higher history buffers in the future
buffer_history_len = shape[1] * 4 if shape[1] < 99 else shape[1] # Allow for higher history buffers in the future
feature_len = shape[2]
self.temporal_buffers[key] = np.zeros((1, buffer_history_len, feature_len), dtype=np.float32)
features_buffer_shape = self.model_runner.input_shapes.get('features_buffer')
if shape[1] in (24, 25) and features_buffer_shape is not None and features_buffer_shape[1] == 24: # 20Hz
buffer_history_len = (features_buffer_shape[1] + 1) * 4
step = int(-buffer_history_len / shape[1])
self.temporal_idxs_map[key] = np.arange(step, step * (shape[1] + 1), step)[::-1]
elif shape[1] == 25: # Split
skip = buffer_history_len // shape[1]
self.temporal_idxs_map[key] = np.arange(buffer_history_len)[-1 - (skip * (shape[1] - 1))::skip]
elif shape[1] == buffer_history_len: # non20hz
self.temporal_idxs_map[key] = np.arange(buffer_history_len)
elif shape[1] >= 99: # non20hz
self.temporal_idxs_map[key] = np.arange(shape[1])
self.temporal_buffers[key] = np.zeros((1, buffer_history_len, feature_len), dtype=np.float32)
@property
def mlsim(self) -> bool:
return bool(self.generation is not None and self.generation >= 11)
@property
def desire_key(self) -> str:
return next(key for key in self.numpy_inputs if key.startswith('desire'))
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire'][0] = 0
new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0)
self.prev_desire[:] = inputs['desire']
self.temporal_buffers['desire'][0,:-1] = self.temporal_buffers['desire'][0,1:]
self.temporal_buffers['desire'][0,-1] = new_desire
inputs[self.desire_key][0] = 0
new_desire = np.where(inputs[self.desire_key] - self.prev_desire > .99, inputs[self.desire_key], 0)
self.prev_desire[:] = inputs[self.desire_key]
self.temporal_buffers[self.desire_key][0,:-1] = self.temporal_buffers[self.desire_key][0,1:]
self.temporal_buffers[self.desire_key][0,-1] = new_desire
# Roll buffer and assign based on desire.shape[1] value
if self.temporal_buffers['desire'].shape[1] > self.numpy_inputs['desire'].shape[1]:
skip = self.temporal_buffers['desire'].shape[1] // self.numpy_inputs['desire'].shape[1]
self.numpy_inputs['desire'][:] = (
self.temporal_buffers['desire'][0].reshape(self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], skip, -1).max(axis=2))
if self.temporal_buffers[self.desire_key].shape[1] > self.numpy_inputs[self.desire_key].shape[1]:
skip = self.temporal_buffers[self.desire_key].shape[1] // self.numpy_inputs[self.desire_key].shape[1]
self.numpy_inputs[self.desire_key][:] = (self.temporal_buffers[self.desire_key][0].reshape(
self.numpy_inputs[self.desire_key].shape[0], self.numpy_inputs[self.desire_key].shape[1], skip, -1).max(axis=2))
else:
self.numpy_inputs['desire'][:] = self.temporal_buffers['desire'][0, self.temporal_idxs_map['desire']]
self.numpy_inputs[self.desire_key][:] = self.temporal_buffers[self.desire_key][0, self.temporal_idxs_map[self.desire_key]]
for key in self.numpy_inputs:
if key in inputs and key not in ['desire']:
if key in inputs and key not in [self.desire_key]:
self.numpy_inputs[key][:] = inputs[key]
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names}
@@ -156,10 +161,11 @@ class ModelState(ModelStateBase):
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim)
if v_ego > self.MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
else:
desired_curvature = prev_action.desiredCurvature
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
if v_ego > self.MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
else:
desired_curvature = prev_action.desiredCurvature
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop))
@@ -202,7 +208,7 @@ def main(demo=False):
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
# messaging
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
publish_state = PublishState()
@@ -306,7 +312,7 @@ def main(demo=False):
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.model_runner.vision_input_names}
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.model_runner.vision_input_names}
inputs:dict[str, np.ndarray] = {
'desire': vec_desire,
model.desire_key: vec_desire,
'traffic_convention': traffic_convention,
}
@@ -322,6 +328,7 @@ def main(demo=False):
modelv2_send = messaging.new_message('modelV2')
drivingdata_send = messaging.new_message('drivingModelData')
posenet_send = messaging.new_message('cameraOdometry')
mdv2sp_send = messaging.new_message('modelDataV2SP')
action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
prev_action = action
@@ -336,6 +343,7 @@ def main(demo=False):
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
@@ -343,6 +351,7 @@ def main(demo=False):
pm.send('modelV2', modelv2_send)
pm.send('drivingModelData', drivingdata_send)
pm.send('cameraOdometry', posenet_send)
pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id
@@ -8,12 +8,16 @@ import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
ModelState = modeld_module.ModelState
# These are the shapes extracted/loaded from the model onnx
SHAPE_MODE_PARAMS = [
({'desire': (1, 25, 8), 'features_buffer': (1, 25, 512), 'prev_desired_curv': (1, 25, 1)}, 'split'),
({'desire': (1, 25, 8), 'features_buffer': (1, 24, 512), 'prev_desired_curv': (1, 25, 1)}, '20hz'),
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1)}, 'non20hz'),
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "nav_features": (1, 256), "nav_instructions": (1, 150)}, 'non20hz'), # Optimus Prime
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "lat_planner_state": (1, 4),}, 'non20hz'), # farmville
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "lateral_control_params": (1, 2), "prev_desired_curv": (1, 100, 1)}, 'non20hz'), # wd40
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1), "lateral_control_params": (1, 2),}, 'non20hz'), # NTS
({'desire': (1, 25, 8), 'features_buffer': (1, 24, 512)}, '20hz'), # NPR
({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1), "lateral_control_params": (1, 2),}, 'non20hz'), # NTS
({'desire': (1, 25, 8), 'features_buffer': (1, 25, 512)}, 'split'), # Steam Powered v2
({'desire_pulse': (1, 25, 8), 'features_buffer': (1, 25, 512)}, 'split'), # desire rename
]
@@ -95,9 +99,7 @@ def get_expected_indices(shape, constants, mode, key=None):
idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1]
return idxs
elif mode == 'non20hz':
if key and shape[1] == constants.FULL_HISTORY_BUFFER_LEN:
return np.arange(constants.FULL_HISTORY_BUFFER_LEN)
return None
return np.arange(shape[1])
return None
@@ -108,6 +110,8 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches):
for key in shapes:
buf = state.temporal_buffers.get(key, None)
idxs = state.temporal_idxs_map.get(key, None)
if buf is None:
continue # not all shapes are 3D, and the non-3D ones are not buffered
# Buffer shape logic
if mode == 'split':
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
@@ -116,10 +120,7 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches):
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
expected_idxs = get_expected_indices(shapes[key], constants, '20hz', key)
elif mode == 'non20hz':
if key == 'features_buffer':
expected_shape = (1, shapes[key][1]*4, shapes[key][2])
else:
expected_shape = (1, shapes[key][1], shapes[key][2])
expected_shape = (1, shapes[key][1], shapes[key][2])
expected_idxs = get_expected_indices(shapes[key], constants, 'non20hz', key)
assert buf is not None, f"{key}: buffer not found"
@@ -130,10 +131,10 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches):
assert idxs is None or idxs.size == 0, f"{key}: buffer idxs should be None or empty"
def legacy_buffer_update(buf, new_val, mode, key, constants, idxs):
def legacy_buffer_update(buf, new_val, mode, key, constants, idxs, input_shape, prev_desire=None):
# This is what we compare the new dynamic logic to, to ensure it does the same thing
if mode == 'split':
if key == 'desire':
if key == 'desire' or key.startswith('desire'):
buf[0,:-1] = buf[0,1:]
buf[0,-1] = new_val
return buf.reshape((1, constants.INPUT_HISTORY_BUFFER_LEN, constants.TEMPORAL_SKIP, -1)).max(axis=2)
@@ -173,15 +174,22 @@ def legacy_buffer_update(buf, new_val, mode, key, constants, idxs):
return legacy_buf[idxs]
elif mode == 'non20hz':
if key == 'desire':
length = new_val.shape[0]
buf[0,:-1,:length] = buf[0,1:,:length]
buf[0,-1,:length] = new_val[:length]
desire_len = constants.DESIRE_LEN
if prev_desire is None:
prev_desire = np.zeros(desire_len, dtype=np.float32)
# Set first element to zero
new_val = new_val.copy()
new_val[0] = 0
# Shift buffer by desire len
buf[0][:-desire_len] = buf[0][desire_len:]
# Only insert new desire if rising edge
buf[0][-desire_len:] = np.where(new_val - prev_desire > 0.99, new_val, 0)
prev_desire[:] = new_val
return buf[0]
elif key == 'features_buffer':
feature_len = new_val.shape[0]
buf[0,:-1,:feature_len] = buf[0,1:,:feature_len]
buf[0,-1,:feature_len] = new_val[:feature_len]
return buf[0]
buf[0, :-1] = buf[0, 1:]
buf[0, -1] = new_val
return buf[0, -input_shape[1]:] # (99, 512)
elif key == 'prev_desired_curv':
length = new_val.shape[0]
buf[0,:-length,0] = buf[0,length:,0]
@@ -191,32 +199,18 @@ def legacy_buffer_update(buf, new_val, mode, key, constants, idxs):
def dynamic_buffer_update(state, key, new_val, mode):
if key == 'desire':
state.temporal_buffers['desire'][0,:-1] = state.temporal_buffers['desire'][0,1:]
state.temporal_buffers['desire'][0,-1] = new_val
if state.temporal_buffers['desire'].shape[1] > state.numpy_inputs['desire'].shape[1]:
skip = state.temporal_buffers['desire'].shape[1] // state.numpy_inputs['desire'].shape[1]
return state.temporal_buffers['desire'][0].reshape(
state.numpy_inputs['desire'].shape[0], state.numpy_inputs['desire'].shape[1], skip, -1
).max(axis=2)
else:
return state.temporal_buffers['desire'][0, state.temporal_idxs_map['desire']]
inputs = {'desire': np.zeros((1, state.constants.DESIRE_LEN), dtype=np.float32)}
for k, tb in state.temporal_buffers.items():
if k in state.temporal_idxs_map:
continue
buf_len = tb.shape[1]
if k in state.numpy_inputs:
out_len = state.numpy_inputs[k].shape[1]
if out_len <= buf_len:
state.temporal_idxs_map[k] = np.arange(buf_len)[-out_len:]
else:
state.temporal_idxs_map[k] = np.arange(buf_len)
else:
state.temporal_idxs_map[k] = np.arange(buf_len)
if key == 'desire' or key.startswith('desire'):
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
for k, v in state.model_runner.input_shapes.items() if k != key}
inputs[key] = new_val.copy()
# ModelState.run expects desire as a pulse, so we zero the first element.
inputs[key][0] = 0
state.run({}, {}, inputs, prepare_only=False)
return state.numpy_inputs[key]
if key == 'features_buffer':
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
for k, v in state.model_runner.input_shapes.items() if k != 'features_buffer'}
def run_model_stub():
return {
'hidden_state': np.asarray(new_val, dtype=np.float32).reshape(1, -1),
@@ -226,6 +220,8 @@ def dynamic_buffer_update(state, key, new_val, mode):
return state.numpy_inputs['features_buffer'][0]
if key == 'prev_desired_curv':
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
for k, v in state.model_runner.input_shapes.items() if k != 'prev_desired_curv'}
def run_model_stub():
return {
'hidden_state': np.zeros((1, state.constants.FEATURE_LEN), dtype=np.float32),
@@ -241,16 +237,27 @@ def dynamic_buffer_update(state, key, new_val, mode):
@pytest.mark.parametrize("key", ["desire", "features_buffer", "prev_desired_curv"])
def test_buffer_update_equivalence(shapes, mode, key, apply_patches):
state = ModelState(None)
if key == "desire":
desire_keys = [k for k in shapes.keys() if k.startswith('desire')]
if desire_keys:
actual_key = desire_keys[0] # Use the first (and likely only) desire key
else:
actual_key = key
if actual_key not in state.numpy_inputs:
pytest.skip()
constants = DummyModelRunner(shapes).constants
buf = state.temporal_buffers.get(key, None)
idxs = state.temporal_idxs_map.get(key, None)
input_shape = shapes[key]
buf = state.temporal_buffers.get(actual_key, None)
idxs = state.temporal_idxs_map.get(actual_key, None)
input_shape = shapes[actual_key]
prev_desire = np.zeros(constants.DESIRE_LEN, dtype=np.float32) if key == 'desire' else None
for step in range(20): # multiple steps to ensure history is built up
new_val = np.full((input_shape[2],), step, dtype=np.float32)
expected = legacy_buffer_update(buf, new_val, mode, key, constants, idxs)
actual = dynamic_buffer_update(state, key, new_val, mode)
# Model returns the reduced numpy_inputs history, compare the last n entries so the test is checking the same slices.
expected = legacy_buffer_update(buf, new_val, mode, actual_key, constants, idxs, input_shape, prev_desire)
actual = dynamic_buffer_update(state, actual_key, new_val, mode)
if expected is not None and actual is not None and expected.shape != actual.shape:
if expected.ndim == 2 and actual.ndim == 2 and expected.shape[1] == actual.shape[1]:
expected = expected[-actual.shape[0]:]
assert np.allclose(actual, expected), f"{mode} {key}: dynamic buffer update does not match legacy logic"
assert np.allclose(actual, expected), f"{mode} {actual_key}: dynamic buffer update does not match legacy logic"
+31 -13
View File
@@ -8,6 +8,7 @@ See the LICENSE.md file in the root directory for more details.
import time
import requests
from requests.exceptions import (SSLError, RequestException, HTTPError)
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from sunnypilot.models.helpers import is_bundle_version_compatible
@@ -122,19 +123,36 @@ class ModelFetcher:
self.model_cache = ModelCache(params)
self.model_parser = ModelParser()
def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle]:
"""Fetches fresh model data from remote and updates cache"""
def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None:
"""Fetches fresh model data from remote and updates cache.
Returns None on transport errors. Raises on 404 and other fatal HTTP errors.
"""
try:
response = requests.get(self.MODEL_URL, timeout=10)
response.raise_for_status()
json_data = response.json()
# Explicitly handle 404 differently
if response.status_code == 404:
cloudlog.error(f"Models URL returned 404 Not Found: {self.MODEL_URL}")
raise HTTPError(f"404 Not Found: {self.MODEL_URL}", response=response)
# Raise for any other 4xx/5xx
response.raise_for_status()
json_data = response.json()
self.model_cache.set(json_data)
cloudlog.debug("Successfully updated models cache")
return self.model_parser.parse_models(json_data)
except Exception:
cloudlog.exception("Error fetching models")
raise
except ConnectionError as e:
cloudlog.warning(f"DNS/connection error while fetching models: {e}")
except SSLError as e:
cloudlog.warning(f"SSL error while fetching models: {e}")
except RequestException as e:
cloudlog.warning(f"Request transport error while fetching models: {e}")
except Exception as e:
cloudlog.exception(f"Unexpected error fetching models: {e}")
return None
def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]:
"""Gets the list of available models, with smart cache handling"""
@@ -144,12 +162,12 @@ class ModelFetcher:
cloudlog.debug("Using valid cached models data")
return self.model_parser.parse_models(cached_data)
try:
return self._fetch_and_cache_models()
except Exception:
if not cached_data:
cloudlog.exception("Failed to fetch fresh data and no cache available")
raise
fetched_bundles = self._fetch_and_cache_models()
if fetched_bundles is not None:
return fetched_bundles
if not cached_data:
cloudlog.warning("Failed to fetch fresh data and no cache available")
cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback")
return self.model_parser.parse_models(cached_data)
+1 -1
View File
@@ -19,7 +19,7 @@ from openpilot.system.hardware.hw import Paths
from pathlib import Path
# see the README.md for more details on the model selector versioning
CURRENT_SELECTOR_VERSION = 9
CURRENT_SELECTOR_VERSION = 10
REQUIRED_MIN_SELECTOR_VERSION = 9
USE_ONNX = os.getenv('USE_ONNX', PC)
@@ -0,0 +1,45 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This 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.common.constants import CV
from openpilot.common.params import Params
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
class LaneTurnController:
def __init__(self, desire_helper):
self.DH = desire_helper
self.turn_direction = custom.TurnDirection.none
self.params = Params()
self.lane_turn_value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
self.param_read_counter = 0
self.enabled = self.params.get_bool("LaneTurnDesire")
def read_params(self):
self.enabled = self.params.get_bool("LaneTurnDesire")
value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
self.lane_turn_value = min(float(LANE_CHANGE_SPEED_MIN), value)
def update_params(self) -> None:
if self.param_read_counter % 50 == 0:
self.read_params()
self.param_read_counter += 1
def update_lane_turn(self, blindspot_left: bool, blindspot_right: bool, left_blinker: bool, right_blinker: bool, v_ego: float) -> None:
if left_blinker and not right_blinker and v_ego < self.lane_turn_value and not blindspot_left:
self.turn_direction = custom.TurnDirection.turnLeft
elif right_blinker and not left_blinker and v_ego < self.lane_turn_value and not blindspot_right:
self.turn_direction = custom.TurnDirection.turnRight
else:
self.turn_direction = custom.TurnDirection.none
def get_turn_direction(self):
if not self.enabled:
return custom.TurnDirection.none
return self.turn_direction
@@ -0,0 +1,113 @@
import pytest
from cereal import log
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
class TurnDirection:
none = 0
turnLeft = 1
turnRight = 2
@pytest.mark.parametrize("left_blinker,right_blinker,v_ego,blindspot_left,blindspot_right,expected", [
(True, False, 5, False, False, TurnDirection.turnLeft),
(False, True, 6, False, False, TurnDirection.turnRight),
(True, False, 9, False, False, TurnDirection.none),
(True, False, 7, True, False, TurnDirection.none),
(False, True, 6, False, True, TurnDirection.none),
(False, False, 5, False, False, TurnDirection.none),
(True, True, 5, False, False, TurnDirection.none),
])
def test_lane_turn_desire_conditions(left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected):
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
assert controller.get_turn_direction() == expected
def test_lane_turn_desire_disabled():
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = False
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(False, False, True, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
def test_lane_turn_overrides_lane_change():
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
# left turn desire
controller.update_lane_turn(False, False, True, False, 5)
assert controller.get_turn_direction() == TurnDirection.turnLeft
# right turn desire
controller.update_lane_turn(False, False, False, True, 6)
assert controller.get_turn_direction() == TurnDirection.turnRight
# no turn
controller.update_lane_turn(False, False, False, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
@pytest.mark.parametrize("v_ego,expected", [
(8.93, TurnDirection.turnLeft), # just below threshold
(8.96, TurnDirection.none), # above threshold
(8.95, TurnDirection.none), # just above threshold
])
def test_lane_turn_desire_speed_boundary(v_ego, expected):
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(False, True, True, False, v_ego)
assert controller.get_turn_direction() == expected
class DummyCarState:
def __init__(self, vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=False, steeringTorque=0, brakePressed=False):
self.vEgo = vEgo
self.leftBlinker = leftBlinker
self.rightBlinker = rightBlinker
self.leftBlindspot = leftBlindspot
self.rightBlindspot = rightBlindspot
self.steeringPressed = steeringPressed
self.steeringTorque = steeringTorque
self.brakePressed = brakePressed
@pytest.fixture
def set_lane_turn_params():
params = Params()
params.put("LaneTurnDesire", True)
params.put("LaneTurnValue", 20.0)
@pytest.mark.parametrize("carstate, lateral_active, lane_change_prob, expected_desire", [
# Lane turn desire overrides lane change desire
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnLeft),
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnRight),
# Lane change desire only (no turn desires)
(DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft),
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight),
# No desire (inactive)
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none),
(DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire!
])
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10):
dh.update(carstate, lateral_active, lane_change_prob)
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
+16 -1
View File
@@ -132,6 +132,21 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
EventNameSP.pedalPressedAlertOnly: {
ET.WARNING: NoEntryAlert("Pedal Pressed")
}
},
EventNameSP.laneTurnLeft: {
ET.WARNING: Alert(
"Turning Left",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.),
},
EventNameSP.laneTurnRight: {
ET.WARNING: Alert(
"Turning Right",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.),
}
}
+41 -21
View File
@@ -3,7 +3,9 @@
from __future__ import annotations
import base64
import errno
import gzip
import json
import os
import ssl
import threading
@@ -17,11 +19,11 @@ from openpilot.common.swaglog import cloudlog
from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \
recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler
from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException,
create_connection)
create_connection, WebSocketConnectionClosedException)
import cereal.messaging as messaging
from sunnypilot.sunnylink.api import SunnylinkApi
from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready
from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai')
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
@@ -107,10 +109,13 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None:
except WebSocketTimeoutException:
ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping
if ns_since_last_ping > SUNNYLINK_RECONNECT_TIMEOUT_S * 1e9:
cloudlog.exception("sunnylinkd.ws_recv.timeout")
cloudlog.warning("sunnylinkd.ws_recv.timeout")
end_event.set()
except Exception:
cloudlog.exception("sunnylinkd.ws_recv.exception")
except Exception as e:
if isinstance(e, WebSocketConnectionClosedException):
cloudlog.warning(f"sunnylinkd.ws_recv.{type(e).__name__}")
else:
cloudlog.exception("sunnylinkd.ws_recv.exception")
end_event.set()
@@ -137,11 +142,15 @@ def ws_queue(end_event: threading.Event) -> None:
sunnylink_api.resume_queued(timeout=29)
resume_requested = True
tries = 0
except Exception:
cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception")
except Exception as e:
if isinstance(e, (ConnectionError, TimeoutError)):
cloudlog.warning(f"sunnylinkd.ws_queue.resume_queued.{type(e).__name__}")
else:
cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception")
resume_requested = False
tries += 1
time.sleep(backoff(tries)) # Wait for the backoff time before the next attempt
time.sleep(backoff(tries))
if end_event.is_set():
cloudlog.debug("end_event is set, exiting ws_queue thread")
@@ -171,16 +180,22 @@ def getParamsAllKeys() -> list[str]:
@dispatcher.add_method
def getParams(params_keys: list[str], compression: bool = False) -> str | dict[str, str]:
params = Params()
try:
params = Params()
params_dict: dict[str, bytes] = {key: params.get(key) or b'' for key in params_keys}
param_keys_validated = [key for key in params_keys if key in getParamsAllKeys()]
params_dict: dict[str, list[dict[str, str | bool | int ]]] = {"params": [
{
"key": key,
"value": base64.b64encode(gzip.compress(get_param_as_byte(key)) if compression else get_param_as_byte(key)).decode('utf-8'),
"type": int(params.get_type(key).value),
"is_compressed": compression
} for key in param_keys_validated
]}
# Compress the values before encoding to base64 as output from params.get is bytes and same for compression
if compression:
params_dict = {key: gzip.compress(value) for key, value in params_dict.items()}
# Last step is to encode the values to base64 and decode to utf-8 for JSON serialization
return {key: base64.b64encode(value).decode('utf-8') for key, value in params_dict.items()}
response = {str(param.get('key')): str(param.get('value')) for param in params_dict.get("params", [])}
response |= {"params": json.dumps(params_dict.get("params", []))} # Upcoming for settings v1
return response
except Exception as e:
cloudlog.exception("sunnylinkd.getParams.exception", e)
@@ -252,14 +267,19 @@ def main(exit_event: threading.Event = None):
handle_long_poll(ws, exit_event)
except (KeyboardInterrupt, SystemExit):
break
except (ConnectionError, TimeoutError, WebSocketException):
except Exception as e:
conn_retries += 1
params.remove("LastSunnylinkPingTime")
except Exception:
cloudlog.exception("sunnylinkd.main.exception")
conn_retries += 1
params.remove("LastSunnylinkPingTime")
if isinstance(e, (ConnectionError, TimeoutError, WebSocketException)):
cloudlog.warning(f"sunnylinkd.main.{type(e).__name__}")
elif isinstance(e, OSError):
name = errno.errorcode.get(e.errno or -1, "UNKNOWN")
msg = f"sunnylinkd.main.OSError.{name} ({e.errno})"
is_expected_error = e.errno in (errno.ENETDOWN, errno.ENETRESET, errno.ENETUNREACH)
cloudlog.warning(msg) if is_expected_error else cloudlog.exception(msg)
else:
cloudlog.exception("sunnylinkd.main.exception")
time.sleep(backoff(conn_retries))
+7 -3
View File
@@ -20,6 +20,7 @@ from openpilot.system.version import get_version
from cereal import messaging, custom
from sunnypilot.sunnylink.api import SunnylinkApi
from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder
from sunnypilot.sunnylink.utils import get_param_as_byte
class OperationType(Enum):
@@ -74,7 +75,7 @@ class BackupManagerSP:
config_data = {}
params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)]
for param in params_to_backup:
value = str(self.params.get(param)).encode('utf-8')
value = get_param_as_byte(param)
if value is not None:
config_data[param] = base64.b64encode(value).decode('utf-8')
return config_data
@@ -113,6 +114,7 @@ class BackupManagerSP:
payload = json.loads(json.dumps(backup_info.to_dict(), cls=SnakeCaseEncoder))
self._update_progress(75.0, OperationType.BACKUP)
cloudlog.debug(f"Uploading backup with payload: {json.dumps(payload)}")
# Upload to sunnylink
result = self.api.api_get(
f"backup/{self.device_id}",
@@ -124,9 +126,11 @@ class BackupManagerSP:
if result:
self.backup_status = custom.BackupManagerSP.Status.completed
self._update_progress(100.0, OperationType.BACKUP)
cloudlog.info("Backup successfully created and uploaded")
else:
self.backup_status = custom.BackupManagerSP.Status.failed
self.last_error = "Failed to upload backup"
cloudlog.error(result)
self._report_status()
return bool(self.backup_status == custom.BackupManagerSP.Status.completed)
@@ -264,8 +268,8 @@ class BackupManagerSP:
# Check for backup command
if self.params.get_bool("BackupManager_CreateBackup"):
try:
await self.create_backup()
reset_progress = True
if await self.create_backup():
reset_progress = True
finally:
self.params.remove("BackupManager_CreateBackup")
+14 -1
View File
@@ -1,5 +1,6 @@
import json
from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.common.params import Params
from openpilot.common.params import Params, ParamKeyType
from openpilot.system.version import is_prebuilt
@@ -55,3 +56,15 @@ def get_api_token():
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
token = sunnylink_api.get_token()
print(f"API Token: {token}")
def get_param_as_byte(param_name: str) -> bytes:
params = Params()
param = params.get(param_name)
param_type = params.get_type(param_name)
if param_type == ParamKeyType.BYTES:
return bytes(param)
elif param_type == ParamKeyType.JSON:
return json.dumps(param).encode('utf-8')
return str(param).encode('utf-8')
@@ -9,3 +9,5 @@ export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="12.8"
fi
export STAGING_ROOT="/data/safe_staging"
+11 -1
View File
@@ -24,7 +24,7 @@ from openpilot.system.statsd import statlog
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.power_monitoring import PowerMonitoring
from openpilot.system.hardware.fan_controller import TiciFanController
from openpilot.system.version import terms_version, training_version
from openpilot.system.version import terms_version, training_version, get_build_metadata
ThermalStatus = log.DeviceState.ThermalStatus
NetworkType = log.DeviceState.NetworkType
@@ -326,6 +326,16 @@ def hardware_thread(end_event, hw_queue) -> None:
startup_conditions["not_always_offroad"] = not offroad_mode
onroad_conditions["not_always_offroad"] = not offroad_mode
# if an unsupported device and branch is detected, going onroad is blocked
# only allow going onroad when:
# - TIZI, or
# - TICI and channel_type is "tici"
build_metadata = get_build_metadata()
is_unsupported_combo = TICI and HARDWARE.get_device_type() == "tici" and build_metadata.channel_type != "tici"
startup_conditions["not_tici"] = not is_unsupported_combo
onroad_conditions["not_tici"] = not is_unsupported_combo
set_offroad_alert("Offroad_TiciSupport", is_unsupported_combo, extra_text=build_metadata.channel)
# if the temperature enters the danger zone, go offroad to cool down
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
extra_text = f"{offroad_comp_temp:.1f}C"
+2 -14
View File
@@ -7,7 +7,6 @@ import psutil
import shutil
import signal
import fcntl
import time
import threading
from collections import defaultdict
from pathlib import Path
@@ -19,7 +18,7 @@ from openpilot.common.markdown import parse_markdown
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.system.hardware import AGNOS, HARDWARE
from openpilot.system.version import get_build_metadata
from openpilot.system.version import get_build_metadata, SP_BRANCH_MIGRATIONS
LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock")
STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging")
@@ -190,15 +189,6 @@ def finalize_update() -> None:
run(["git", "reset", "--hard"], FINALIZED)
run(["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], FINALIZED)
cloudlog.info("Starting git cleanup in finalized update")
t = time.monotonic()
try:
run(["git", "gc"], FINALIZED)
run(["git", "lfs", "prune"], FINALIZED)
cloudlog.event("Done git cleanup", duration=time.monotonic() - t)
except subprocess.CalledProcessError:
cloudlog.exception(f"Failed git cleanup, took {time.monotonic() - t:.3f} s")
set_consistent_flag(True)
cloudlog.info("done finalizing overlay")
@@ -242,9 +232,7 @@ class Updater:
b: str | None = self.params.get("UpdaterTargetBranch")
if b is None:
b = self.get_branch(BASEDIR)
b = {
("tici", "release3"): "release-tici"
}.get((HARDWARE.get_device_type(), b), b)
b = SP_BRANCH_MIGRATIONS.get((HARDWARE.get_device_type(), b), b)
return b
@property
+13 -3
View File
@@ -11,11 +11,18 @@ from openpilot.common.swaglog import cloudlog
from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date
RELEASE_SP_BRANCHES = ['release-c3']
TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new']
TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new', 'staging-tici']
MASTER_SP_BRANCHES = ['master']
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES
SP_BRANCH_MIGRATIONS = {
("tici", "staging-c3-new"): "staging-tici",
("tici", "dev-c3-new"): "staging-tici",
("tici", "master"): "master-tici",
("tici", "master-dev-c3-new"): "master-tici",
}
BUILD_METADATA_FILENAME = "build.json"
training_version: str = "0.2.0"
@@ -85,7 +92,8 @@ class OpenpilotMetadata:
@property
def sunnypilot_remote(self) -> bool:
return self.git_normalized_origin == "github.com/sunnypilot/sunnypilot"
return self.git_normalized_origin in ("github.com/sunnypilot/sunnypilot",
"github.com/sunnypilot/openpilot")
@property
def git_normalized_origin(self) -> str:
@@ -127,7 +135,9 @@ class BuildMetadata:
@property
def channel_type(self) -> str:
if self.development_channel:
if self.channel.endswith("-tici"):
return "tici"
elif self.development_channel:
return "development"
elif self.channel.startswith("staging-"):
return "staging"