Compare commits

..

1 Commits

Author SHA1 Message Date
DevTekVE be63625fa8 Add Dockerfile and related scripts for sunnypilot CI build pipeline
This commit introduces `Dockerfile.sunnypilot`, `.dockerignore` updates, and a new `docker_build_sp.sh` script to support building and testing sunnypilot in a Dockerized environment.
2025-07-01 18:26:16 +02:00
117 changed files with 1941 additions and 3220 deletions
-1
View File
@@ -2,4 +2,3 @@ Wen
REGIST REGIST
PullRequest PullRequest
cancelled cancelled
FOF
+13
View File
@@ -18,6 +18,19 @@
venv/ venv/
.venv/ .venv/
**/.idea
**/.hypothesis
**/.mypy_cache
**/.venv
**/.venv/
**/.ci_cache
**/*.rlog
**/Dockerfile*
**/dockerfile*
**/build_output
notebooks notebooks
phone phone
@@ -1,142 +0,0 @@
name: Build All Tinygrad Models and Push to GitLab
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to run workflow from'
required: false
default: 'master-new'
type: string
jobs:
setup:
runs-on: ubuntu-latest
outputs:
json_file: ${{ steps.get-json.outputs.json_file }}
model_matrix: ${{ steps.set-matrix.outputs.model_matrix }}
steps:
- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
- name: Add GitLab.com SSH key to known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
- name: Checkout docs repo (sunnypilot-docs, gh-pages)
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-docs
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
- name: Clone GitLab docs repo
env:
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
run: |
echo "Cloning GitLab"
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
cd gitlab_docs
git checkout main
cd ..
- name: Set next recompiled dir
id: set-recompiled
run: |
cd gitlab_docs
echo "checkout models/"
git sparse-checkout set --no-cone models/
git checkout main
cd models
latest_dir=$(ls -d recompiled* 2>/dev/null | sed -E 's/recompiled([0-9]+)/\1/' | sort -n | tail -1)
if [[ -z "$latest_dir" ]]; then
next_dir=1
else
next_dir=$((latest_dir+1))
fi
recompiled_dir="recompiled${next_dir}"
if [ -d "$recompiled_dir" ]; then
echo "ERROR: $recompiled_dir already exists in GitLab repo"
exit 1
fi
mkdir -p "$recompiled_dir"
touch "$recompiled_dir/.gitkeep"
echo "RECOMPILED_DIR=$recompiled_dir" >> $GITHUB_ENV
echo "Created new recompiled dir: $recompiled_dir"
cd ../..
- name: Get next JSON version to use (from GitHub docs repo)
id: get-json
run: |
cd docs/docs
latest=$(ls driving_models_v*.json | sed -E 's/.*_v([0-9]+)\.json/\1/' | sort -n | tail -1)
next=$((latest+1))
json_file="driving_models_v${next}.json"
cp "driving_models_v${latest}.json" "$json_file"
echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT
echo "SRC_JSON_FILE=docs/docs/driving_models_v${latest}.json" >> $GITHUB_ENV
- name: Extract tinygrad models
id: set-matrix
working-directory: docs/docs
run: |
jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name, is_20hz}]' "$(basename "${SRC_JSON_FILE}")" > matrix.json
echo "model_matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT
get_and_build:
needs: [setup]
uses: ./.github/workflows/sunnypilot-build-model.yaml
strategy:
matrix:
model: ${{ fromJson(needs.setup.outputs.model_matrix) }}
name: Model [${{ matrix.model.display_name }}] from ref [${{ matrix.model.ref }}]
with:
upstream_branch: ${{ matrix.model.ref }}
custom_name: ${{ matrix.model.display_name }}
is_20hz: ${{ matrix.model.is_20hz || true }}
secrets: inherit
push_results:
needs: [setup, get_and_build ]
runs-on: ubuntu-latest
steps:
- name: Download all model artifacts
run: |
ARTIFACT_DIR="gitlab_docs/models/$RECOMPILED_DIR"
mkdir -p "$ARTIFACT_DIR"
for name in $(ls ${{ github.workspace }}/output); do
mkdir -p "$ARTIFACT_DIR/$name"
cp -r ${{ github.workspace }}/output/$name/* "$ARTIFACT_DIR/$name/"
done
- name: Push recompiled dir to GitLab
env:
GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
run: |
cd gitlab_docs
git checkout main
mkdir -p models/"$(basename $RECOMPILED_DIR)"
git add models/"$(basename $RECOMPILED_DIR)"
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git commit -m "Add $(basename $RECOMPILED_DIR) from build-all-tinygrad-models"
git push origin main
- name: Run json_parser.py to update JSON
run: |
python3 docs/json_parser.py \
--json-path "${{ needs.setup.outputs.json_file }}" \
--recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR"
- name: Push updated JSON to GitHub docs repo
run: |
cd docs
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git checkout gh-pages
git add docs/"$(basename ${{ needs.setup.outputs.json_file }})"
git commit -m "Create $(basename ${{ needs.setup.outputs.json_file }}) after recompiling models" || echo "No changes to commit"
git push origin gh-pages
@@ -1,206 +0,0 @@
name: Build Single Tinygrad Model and Push
on:
workflow_dispatch:
inputs:
runner_type:
description: 'Runner type'
required: false
default: '[self-hosted, james-mac]'
type: choice
options:
- ubuntu-latest
- '[self-hosted, james-mac]'
build_model_ref:
description: 'Branch to use for build-model workflow'
required: false
default: 'master-new'
type: string
upstream_branch:
description: 'Upstream commit to build from'
required: true
type: string
custom_name:
description: 'Custom name for the model (no date, only name)'
required: false
type: string
recompiled_dir:
description: 'Existing recompiled directory number (e.g. 3 for recompiled3)'
required: true
type: number
json_version:
description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)'
required: true
type: number
model_folder:
description: 'Model folder'
required: true
type: choice
options:
- Simple Plan Models
- TR Models
- DTR Models
- Custom Merge Models
- FOF series models
- Other
custom_model_folder:
description: 'Custom model folder name (if "Other" selected)'
required: false
type: string
generation:
description: 'Model generation'
required: false
type: number
version:
description: 'Minimum selector version'
required: false
type: number
jobs:
build-single:
runs-on: ${{ github.event.inputs.runner_type }}
env:
RECOMPILED_DIR: recompiled${{ github.event.inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_v${{ github.event.inputs.json_version }}.json
steps:
- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
- name: Add GitLab.com SSH key to known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
- name: Clone GitLab docs repo
env:
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
run: |
echo "Cloning GitLab"
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
cd gitlab_docs
echo "checkout models/${RECOMPILED_DIR}"
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
git checkout main
cd ..
- name: Checkout docs repo
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-docs
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
- name: Validate recompiled dir and JSON version
run: |
if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then
echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo"
exit 1
fi
if [ ! -f "$JSON_FILE" ]; then
echo "JSON file $JSON_FILE does not exist!"
exit 1
fi
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq gh
- name: Build model
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
gh workflow run sunnypilot-build-model.yaml \
--repo sunnypilot/sunnypilot \
--ref "${{ github.event.inputs.build_model_ref }}" \
-f upstream_branch="${{ github.event.inputs.upstream_branch }}" \
-f custom_name="${{ github.event.inputs.custom_name }}"
for i in {1..24}; do
RUN_ID=$(gh run list --repo sunnypilot/sunnypilot --workflow=sunnypilot-build-model.yaml --branch="${{ github.event.inputs.build_model_ref }}" --created ">$START_TIME" --limit=1 --json databaseId --jq '.[0].databaseId')
if [ -n "$RUN_ID" ]; then
break
fi
sleep 5
done
if [ -z "$RUN_ID" ]; then
echo "Could not find the triggered workflow run."
exit 1
fi
echo "Watching run ID: $RUN_ID"
gh run watch "$RUN_ID" --repo sunnypilot/sunnypilot
CONCLUSION=$(gh run view "$RUN_ID" --repo sunnypilot/sunnypilot --json conclusion --jq '.conclusion')
echo "Run concluded with: $CONCLUSION"
if [[ "$CONCLUSION" != "success" ]]; then
echo "Workflow run failed with conclusion: $CONCLUSION"
exit 1
fi
- name: Download and extract model artifact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ARTIFACT_DIR="gitlab_docs/models/$RECOMPILED_DIR"
RUN_ID=$(gh run list --repo sunnypilot/sunnypilot --workflow=sunnypilot-build-model.yaml --branch="${{ github.event.inputs.build_model_ref }}" --limit=1 --json databaseId --jq '.[0].databaseId')
ARTIFACT_NAME=$(gh api repos/sunnypilot/sunnypilot/actions/runs/$RUN_ID/artifacts --jq '.artifacts[] | select(.name | startswith("model-")) | .name')
echo "Downloading artifact: $ARTIFACT_NAME from run: $RUN_ID"
mkdir -p "$ARTIFACT_DIR/$ARTIFACT_NAME"
echo "Created directory: $ARTIFACT_DIR/$ARTIFACT_NAME"
gh run download "$RUN_ID" --repo sunnypilot/sunnypilot -n "$ARTIFACT_NAME" --dir "$ARTIFACT_DIR/$ARTIFACT_NAME"
echo "Downloaded artifact zip(s) to: $ARTIFACT_DIR/$ARTIFACT_NAME"
ZIP_PATH=$(find "$ARTIFACT_DIR/$ARTIFACT_NAME" -type f -name '*.zip' | head -n1)
if [ -n "$ZIP_PATH" ]; then
echo "Unzipping $ZIP_PATH to $ARTIFACT_DIR/$ARTIFACT_NAME"
unzip -o "$ZIP_PATH" -d "$ARTIFACT_DIR/$ARTIFACT_NAME"
rm -f "$ZIP_PATH"
echo "Unzipped and removed $ZIP_PATH"
else
echo "No zip file found in $ARTIFACT_DIR/$ARTIFACT_NAME"
fi
echo "Done processing $ARTIFACT_NAME"
- name: Push recompiled dir to GitLab
env:
GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
run: |
cd gitlab_docs
git checkout main
for d in models/"$RECOMPILED_DIR"/*/; do
git sparse-checkout add "$d"
done
git add models/"$RECOMPILED_DIR"
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git commit -m "Update $RECOMPILED_DIR with new/updated model from build-single-tinygrad-model" || echo "No changes to commit"
git push origin main
- name: Run json_parser.py to update JSON
run: |
FOLDER="${{ github.event.inputs.model_folder }}"
if [ "$FOLDER" = "Other" ]; then
FOLDER="${{ github.event.inputs.custom_model_folder }}"
fi
ARGS=""
[ -n "$FOLDER" ] && ARGS="$ARGS --model-folder \"$FOLDER\""
[ -n "${{ github.event.inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ github.event.inputs.generation }}\""
[ -n "${{ github.event.inputs.version }}" ] && ARGS="$ARGS --version \"${{ github.event.inputs.version }}\""
eval python3 docs/json_parser.py \
--json-path "$JSON_FILE" \
--recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \
$ARGS
- name: Push updated JSON to GitHub docs repo
run: |
cd docs
git config --global user.name "GitHub Action"
git config --global user.email "action@github.com"
git checkout gh-pages
git add docs/"$(basename $JSON_FILE)"
git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit"
git push origin gh-pages
+24 -4
View File
@@ -29,7 +29,7 @@ env:
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 -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 --durations-min=5 -n logical PYTEST: pytest --continue-on-collection-errors --cov --cov-report=xml --cov-append --durations=0 --durations-min=5 --hypothesis-seed 0 -n logical
jobs: jobs:
build_release: build_release:
@@ -163,6 +163,12 @@ jobs:
./selfdrive/ui/tests/create_test_translations.sh && \ ./selfdrive/ui/tests/create_test_translations.sh && \
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \
chmod -R 777 /tmp/comma_download_cache" chmod -R 777 /tmp/comma_download_cache"
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v4
with:
name: ${{ github.job }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
process_replay: process_replay:
name: process replay name: process replay
@@ -186,8 +192,10 @@ jobs:
- name: Run replay - name: Run replay
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && 1 || 20 }} timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && 1 || 20 }}
run: | run: |
${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ ${{ env.RUN }} "coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache" chmod -R 777 /tmp/comma_download_cache && \
coverage combine && \
coverage xml"
- name: Print diff - name: Print diff
id: print-diff id: print-diff
if: always() if: always()
@@ -199,7 +207,7 @@ jobs:
name: process_replay_diff.txt name: process_replay_diff.txt
path: selfdrive/test/process_replay/diff.txt path: selfdrive/test/process_replay/diff.txt
- name: Upload reference logs - name: Upload reference logs
if: false # TODO: move this to github instead of azure if: ${{ failure() && steps.print-diff.outcome == 'success' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }}
run: | run: |
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" ${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- name: Run regen - name: Run regen
@@ -208,6 +216,12 @@ jobs:
run: | run: |
${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \
chmod -R 777 /tmp/comma_download_cache" chmod -R 777 /tmp/comma_download_cache"
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v4
with:
name: ${{ github.job }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test_cars: test_cars:
name: cars name: cars
@@ -238,6 +252,12 @@ jobs:
env: env:
NUM_JOBS: 4 NUM_JOBS: 4
JOB_ID: ${{ matrix.job }} JOB_ID: ${{ matrix.job }}
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v4
with:
name: ${{ github.job }}-${{ matrix.job }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
car_docs_diff: car_docs_diff:
name: PR comments name: PR comments
-23
View File
@@ -7,7 +7,6 @@ on:
env: env:
DAYS_BEFORE_PR_CLOSE: 2 DAYS_BEFORE_PR_CLOSE: 2
DAYS_BEFORE_PR_STALE: 9 DAYS_BEFORE_PR_STALE: 9
DAYS_BEFORE_PR_STALE_DRAFT: 30
jobs: jobs:
stale: stale:
@@ -25,28 +24,6 @@ jobs:
exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}
exempt-draft-pr: false
# issue config
days-before-issue-stale: -1 # ignore issues for now
# same as above, but give draft PRs more time
stale_drafts:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
exempt-all-milestones: true
# pull request config
stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.'
close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.'
stale-pr-label: stale
delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo
exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }}
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}
exempt-draft-pr: true
# issue config # issue config
days-before-issue-stale: -1 # ignore issues for now days-before-issue-stale: -1 # ignore issues for now
+10 -45
View File
@@ -9,22 +9,6 @@ env:
MODELS_DIR: ${{ github.workspace }}/selfdrive/modeld/models MODELS_DIR: ${{ github.workspace }}/selfdrive/modeld/models
on: on:
workflow_call:
inputs:
upstream_branch:
description: 'Upstream branch to build from'
required: true
default: 'master'
type: string
custom_name:
description: 'Custom name for the model (no date, only name)'
required: false
type: string
is_20hz:
description: 'Is this a 20Hz model'
required: false
type: boolean
default: true
workflow_dispatch: workflow_dispatch:
inputs: inputs:
upstream_branch: upstream_branch:
@@ -48,53 +32,34 @@ run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from
jobs: jobs:
get_model: get_model:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
REF: ${{ inputs.upstream_branch }}
outputs: outputs:
model_date: ${{ steps.commit-date.outputs.model_date }} model_date: ${{ steps.commit-date.outputs.model_date }}
steps: steps:
# Note: To allow dynamic models from both openpilot and sunnypilot (merges/mashups), we try commaai as default, - uses: actions/checkout@v4
# and fallback to sunnypilot if the ref checkout fails.
- name: Checkout commaai/openpilot
id: checkout_upstream
continue-on-error: true
uses: actions/checkout@v4
with: with:
repository: commaai/openpilot repository: ${{ env.UPSTREAM_REPO }}
ref: ${{ inputs.upstream_branch }} ref: ${{ github.event.inputs.upstream_branch }}
submodules: recursive submodules: recursive
path: openpilot
- name: Fallback to sunnypilot/sunnypilot
if: steps.checkout_upstream.outcome == 'failure'
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot
ref: ${{ inputs.upstream_branch }}
submodules: recursive
path: openpilot
- name: Get commit date - name: Get commit date
id: commit-date id: commit-date
run: | run: |
cd ${{ github.workspace }}/openpilot # Get the commit date in YYYY-MM-DD format
commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y')
echo "model_date=${commit_date}" >> $GITHUB_OUTPUT echo "model_date=${commit_date}" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT cat $GITHUB_OUTPUT
- run: | - run: git lfs pull
cd ${{ github.workspace }}/openpilot
git lfs pull
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: models-${{ env.REF }} name: models
path: ${{ github.workspace }}/openpilot/selfdrive/modeld/models/*.onnx path: ${{ github.workspace }}/selfdrive/modeld/models/*.onnx
build_model: build_model:
runs-on: self-hosted runs-on: self-hosted
needs: get_model needs: get_model
env: env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
REF: ${{ inputs.upstream_branch }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -106,7 +71,7 @@ jobs:
with: with:
path: ${{env.SCONS_CACHE_DIR}} path: ${{env.SCONS_CACHE_DIR}}
key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }}
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) # 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. # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden.
restore-keys: | restore-keys: |
scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model
@@ -149,7 +114,7 @@ jobs:
- name: Download model artifacts - name: Download model artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: models-${{ env.REF }} name: models
path: ${{ github.workspace }}/selfdrive/modeld/models path: ${{ github.workspace }}/selfdrive/modeld/models
- name: Build Model - name: Build Model
+2
View File
@@ -70,6 +70,8 @@ flycheck_*
cppcheck_report.txt cppcheck_report.txt
comma*.sh comma*.sh
selfdrive/modeld/thneed/compile
selfdrive/modeld/models/*.thneed
selfdrive/modeld/models/*.pkl selfdrive/modeld/models/*.pkl
sunnypilot/modeld*/thneed/compile sunnypilot/modeld*/thneed/compile
sunnypilot/modeld*/models/*.thneed sunnypilot/modeld*/models/*.thneed
+2 -2
View File
@@ -2,7 +2,7 @@
<tool name="uv Scons Build Debug" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="false" showConsoleOnStdErr="false" synchronizeAfterRun="true"> <tool name="uv Scons Build Debug" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="false" showConsoleOnStdErr="false" synchronizeAfterRun="true">
<exec> <exec>
<option name="COMMAND" value="bash" /> <option name="COMMAND" value="bash" />
<option name="PARAMETERS" value="-c &quot;source .venv/bin/activate &amp;&amp; scons -u -j$(nproc) --ccflags=\&quot;-fno-inline\&quot;&quot;" /> <option name="PARAMETERS" value="-c &quot;source .venv/bin/activate &amp;&amp; scons -u -j$(nproc) --compile_db --ccflags=\&quot;-fno-inline\&quot;&quot;" />
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" /> <option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec> </exec>
</tool> </tool>
@@ -16,7 +16,7 @@
<tool name="uv Scons Build Release" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="false" showConsoleOnStdErr="false" synchronizeAfterRun="true"> <tool name="uv Scons Build Release" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="false" showConsoleOnStdErr="false" synchronizeAfterRun="true">
<exec> <exec>
<option name="COMMAND" value="bash" /> <option name="COMMAND" value="bash" />
<option name="PARAMETERS" value="-c &quot;source .venv/bin/activate &amp;&amp; scons -u -j$(nproc)&quot; " /> <option name="PARAMETERS" value="-c &quot;source .venv/bin/activate &amp;&amp; scons -u -j$(nproc) --compile_db&quot; " />
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" /> <option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec> </exec>
</tool> </tool>
+66
View File
@@ -0,0 +1,66 @@
FROM sunnypilot-base
ARG RUNNER_DEBUG=0
ENV PYTHONUNBUFFERED=1
ENV OPENPILOT_SRC_PATH=/tmp/openpilot
ENV BUILD_DIR=/data/openpilot
ENV OUTPUT_DIR=/output
RUN sudo apt update && sudo apt install -y rsync
RUN mkdir -p ${OPENPILOT_SRC_PATH}
RUN mkdir -p ${BUILD_DIR}
COPY . ${OPENPILOT_SRC_PATH}
ENV PYTHONPATH=${BUILD_DIR}
WORKDIR ${OPENPILOT_SRC_PATH}
RUN ./tools/ubuntu_setup.sh
RUN ./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/
WORKDIR ${BUILD_DIR}
RUN sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py
RUN scons --cache-readonly -j$(nproc) --minimal
RUN touch ${BUILD_DIR}/prebuilt
RUN sudo rm -rf ${OUTPUT_DIR}
RUN mkdir -p ${OUTPUT_DIR}
ENTRYPOINT [\
"rsync", \
"-am", \
"--include=**/panda/board/", \
"--include=**/panda/board/obj", \
"--include=**/panda/board/obj/panda.bin.signed", \
"--include=**/panda/board/obj/panda_h7.bin.signed", \
"--include=**/panda/board/obj/bootstub.panda.bin", \
"--include=**/panda/board/obj/bootstub.panda_h7.bin", \
"--exclude=.sconsign.dblite", \
"--exclude=*.a", \
"--exclude=*.o", \
"--exclude=*.os", \
"--exclude=*.pyc", \
"--exclude=moc_*", \
"--exclude=*.cc", \
"--exclude=Jenkinsfile", \
"--exclude=supercombo.onnx", \
"--exclude=**/panda/board/*", \
"--exclude=**/panda/board/obj/**", \
"--exclude=**/panda/certs/", \
"--exclude=**/panda/crypto/", \
"--exclude=**/release/", \
"--exclude=**/.github/", \
"--exclude=**/selfdrive/ui/replay/", \
"--exclude=**/__pycache__/", \
"--exclude=**/selfdrive/ui/*.h", \
"--exclude=**/selfdrive/ui/**/*.h", \
"--exclude=**/selfdrive/ui/qt/offroad/sunnypilot/", \
#"--exclude=${SCONS_CACHE_DIR:-}", \
"--exclude=**/.git/", \
"--exclude=**/SConstruct", \
"--exclude=**/SConscript", \
"--exclude=**/.venv/", \
"--delete-excluded", \
"--chown=1000:1000", \
"/data/openpilot/", \
"/output/" \
]
+1 -1
View File
@@ -73,7 +73,7 @@ By default, sunnypilot uploads the driving data to comma servers. You can also a
sunnypilot is open source software. The user is free to disable data collection if they wish to do so. sunnypilot is open source software. The user is free to disable data collection if they wish to do so.
sunnypilot logs the road-facing camera, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs. sunnypilot logs the road-facing camera, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs.
The driver-facing camera and microphone are only logged if you explicitly opt-in in settings. The driver-facing camera is only logged if you explicitly opt-in in settings. The microphone is not recorded.
By using this software, you understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data. By using this software, you understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data.
+1 -7
View File
@@ -1,11 +1,5 @@
Version 0.10.0 (2025-07-07) Version 0.9.10 (2025-06-30)
======================== ========================
* New driving model
* Lead car ground-truth fixes
* Ported over VAE from the MLSIM stack
* New training architecture described in CVPR paper
* Enable live-learned steering actuation delay
* Opt-in audio recording for dashcam video
Version 0.9.9 (2025-05-23) Version 0.9.9 (2025-05-23)
======================== ========================
+6 -1
View File
@@ -39,6 +39,10 @@ AddOption('--clazy',
action='store_true', action='store_true',
help='build with clazy') help='build with clazy')
AddOption('--compile_db',
action='store_true',
help='build clang compilation database')
AddOption('--ccflags', AddOption('--ccflags',
action='store', action='store',
type='string', type='string',
@@ -230,7 +234,8 @@ if arch == "Darwin":
darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]] darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]]
env["LINKFLAGS"] += darwin_rpath_link_flags env["LINKFLAGS"] += darwin_rpath_link_flags
env.CompilationDatabase('compile_commands.json') if GetOption('compile_db'):
env.CompilationDatabase('compile_commands.json')
# Setup cache dir # Setup cache dir
default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache' default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache'
+4 -11
View File
@@ -1083,7 +1083,7 @@ struct ModelDataV2 {
confidence @23: ConfidenceClass; confidence @23: ConfidenceClass;
# Model perceived motion # Model perceived motion
temporalPoseDEPRECATED @21 :Pose; temporalPose @21 :Pose;
# e2e lateral planner # e2e lateral planner
action @26: Action; action @26: Action;
@@ -2470,19 +2470,13 @@ struct DebugAlert {
struct UserFlag { struct UserFlag {
} }
struct SoundPressure @0xdc24138990726023 { struct Microphone {
soundPressure @0 :Float32; soundPressure @0 :Float32;
# uncalibrated, A-weighted # uncalibrated, A-weighted
soundPressureWeighted @3 :Float32; soundPressureWeighted @3 :Float32;
soundPressureWeightedDb @1 :Float32; soundPressureWeightedDb @1 :Float32;
filteredSoundPressureWeightedDb @2 :Float32;
filteredSoundPressureWeightedDbDEPRECATED @2 :Float32;
}
struct AudioData {
data @0 :Data;
sampleRate @1 :UInt32;
} }
struct Touch { struct Touch {
@@ -2562,8 +2556,7 @@ struct Event {
livestreamDriverEncodeIdx @119 :EncodeIndex; livestreamDriverEncodeIdx @119 :EncodeIndex;
# microphone data # microphone data
soundPressure @103 :SoundPressure; microphone @103 :Microphone;
rawAudioData @147 :AudioData;
# systems stuff # systems stuff
androidLog @20 :AndroidLogEntry; androidLog @20 :AndroidLogEntry;
+1 -2
View File
@@ -73,8 +73,7 @@ _services: dict[str, tuple] = {
"navThumbnail": (True, 0.), "navThumbnail": (True, 0.),
"qRoadEncodeIdx": (False, 20.), "qRoadEncodeIdx": (False, 20.),
"userFlag": (True, 0., 1), "userFlag": (True, 0., 1),
"soundPressure": (True, 10., 10), "microphone": (True, 10., 10),
"rawAudioData": (False, 20.),
# sunnypilot # sunnypilot
"modelManagerSP": (False, 1., 1), "modelManagerSP": (False, 1., 1),
+13
View File
@@ -0,0 +1,13 @@
comment: false
coverage:
status:
project:
default:
informational: true
patch: off
ignore:
- "**/test_*.py"
- "selfdrive/test/**"
- "system/version.py" # codecov changes depending on if we are in a branch or not
- "tools"
+1 -1
View File
@@ -1 +1 @@
#define DEFAULT_MODEL "Tomb Raider 14 (Default)" #define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)"
+10 -19
View File
@@ -5,8 +5,8 @@
inline static std::unordered_map<std::string, uint32_t> keys = { inline static std::unordered_map<std::string, uint32_t> keys = {
{"AccessToken", CLEAR_ON_MANAGER_START | DONT_LOG}, {"AccessToken", CLEAR_ON_MANAGER_START | DONT_LOG},
{"AdbEnabled", PERSISTENT | BACKUP}, {"AdbEnabled", PERSISTENT},
{"AlwaysOnDM", PERSISTENT | BACKUP}, {"AlwaysOnDM", PERSISTENT},
{"ApiCache_Device", PERSISTENT}, {"ApiCache_Device", PERSISTENT},
{"ApiCache_FirehoseStats", PERSISTENT}, {"ApiCache_FirehoseStats", PERSISTENT},
{"AssistNowToken", PERSISTENT}, {"AssistNowToken", PERSISTENT},
@@ -78,7 +78,7 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"LocationFilterInitialState", PERSISTENT}, {"LocationFilterInitialState", PERSISTENT},
{"LongitudinalManeuverMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"LongitudinalManeuverMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"LongitudinalPersonality", PERSISTENT | BACKUP}, {"LongitudinalPersonality", PERSISTENT | BACKUP},
{"NetworkMetered", PERSISTENT | BACKUP}, {"NetworkMetered", PERSISTENT},
{"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"Offroad_BadNvme", CLEAR_ON_MANAGER_START}, {"Offroad_BadNvme", CLEAR_ON_MANAGER_START},
@@ -99,10 +99,9 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"PandaSomResetTriggered", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"PandaSomResetTriggered", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"PandaSignatures", CLEAR_ON_MANAGER_START}, {"PandaSignatures", CLEAR_ON_MANAGER_START},
{"PrimeType", PERSISTENT}, {"PrimeType", PERSISTENT},
{"RecordAudio", PERSISTENT | BACKUP},
{"RecordFront", PERSISTENT | BACKUP}, {"RecordFront", PERSISTENT | BACKUP},
{"RecordFrontLock", PERSISTENT}, // for the internal fleet {"RecordFrontLock", PERSISTENT}, // for the internal fleet
{"SecOCKey", PERSISTENT | DONT_LOG | BACKUP}, {"SecOCKey", PERSISTENT | DONT_LOG}, // Candidate for | BACKUP
{"RouteCount", PERSISTENT}, {"RouteCount", PERSISTENT},
{"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"SshEnabled", PERSISTENT | BACKUP}, {"SshEnabled", PERSISTENT | BACKUP},
@@ -124,29 +123,24 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// --- sunnypilot params --- // // --- sunnypilot params --- //
{"ApiCache_DriveStats", PERSISTENT}, {"ApiCache_DriveStats", PERSISTENT},
{"AutoLaneChangeBsmDelay", PERSISTENT | BACKUP}, {"AutoLaneChangeBsmDelay", PERSISTENT},
{"AutoLaneChangeTimer", PERSISTENT | BACKUP}, {"AutoLaneChangeTimer", PERSISTENT},
{"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP}, {"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP},
{"BlinkerPauseLateralControl", PERSISTENT | BACKUP}, {"BlinkerPauseLateralControl", PERSISTENT | BACKUP},
{"Brightness", PERSISTENT | BACKUP},
{"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"CarParamsSPCache", CLEAR_ON_MANAGER_START}, {"CarParamsSPCache", CLEAR_ON_MANAGER_START},
{"CarParamsSPPersistent", PERSISTENT}, {"CarParamsSPPersistent", PERSISTENT},
{"CarPlatformBundle", PERSISTENT | BACKUP}, {"CarPlatformBundle", PERSISTENT},
{"ChevronInfo", PERSISTENT | BACKUP},
{"CustomAccIncrementsEnabled", PERSISTENT | BACKUP}, {"CustomAccIncrementsEnabled", PERSISTENT | BACKUP},
{"CustomAccLongPressIncrement", PERSISTENT | BACKUP}, {"CustomAccLongPressIncrement", PERSISTENT | BACKUP},
{"CustomAccShortPressIncrement", PERSISTENT | BACKUP}, {"CustomAccShortPressIncrement", PERSISTENT | BACKUP},
{"DeviceBootMode", PERSISTENT | BACKUP}, {"DeviceBootMode", PERSISTENT | BACKUP},
{"EnableGithubRunner", PERSISTENT | BACKUP}, {"EnableGithubRunner", PERSISTENT | BACKUP},
{"InteractivityTimeout", PERSISTENT | BACKUP},
{"IsDevelopmentBranch", CLEAR_ON_MANAGER_START},
{"MaxTimeOffroad", PERSISTENT | BACKUP}, {"MaxTimeOffroad", PERSISTENT | BACKUP},
{"Brightness", PERSISTENT | BACKUP},
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
{"OffroadMode", CLEAR_ON_MANAGER_START}, {"OffroadMode", CLEAR_ON_MANAGER_START},
{"QuickBootToggle", PERSISTENT | BACKUP},
{"QuietMode", PERSISTENT | BACKUP}, {"QuietMode", PERSISTENT | BACKUP},
{"ShowAdvancedControls", PERSISTENT | BACKUP},
// MADS params // MADS params
{"Mads", PERSISTENT | BACKUP}, {"Mads", PERSISTENT | BACKUP},
@@ -156,7 +150,6 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// Model Manager params // Model Manager params
{"ModelManager_ActiveBundle", PERSISTENT}, {"ModelManager_ActiveBundle", PERSISTENT},
{"ModelManager_ClearCache", CLEAR_ON_MANAGER_START},
{"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"ModelManager_ModelsCache", PERSISTENT | BACKUP}, {"ModelManager_ModelsCache", PERSISTENT | BACKUP},
@@ -178,15 +171,13 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"BackupManager_RestoreVersion", PERSISTENT}, {"BackupManager_RestoreVersion", PERSISTENT},
// sunnypilot car specific params // sunnypilot car specific params
{"HyundaiLongitudinalTuning", PERSISTENT | BACKUP}, {"HyundaiLongitudinalTuning", PERSISTENT},
{"DynamicExperimentalControl", PERSISTENT | BACKUP}, {"DynamicExperimentalControl", PERSISTENT},
{"BlindSpot", PERSISTENT | BACKUP}, {"BlindSpot", PERSISTENT | BACKUP},
// model panel params // model panel params
{"LagdToggle", PERSISTENT | BACKUP}, {"LagdToggle", PERSISTENT | BACKUP},
{"LagdToggleDesc", PERSISTENT},
{"LagdToggledelay", PERSISTENT | BACKUP},
// mapd // mapd
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, {"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
+1 -1
View File
@@ -1 +1 @@
#define COMMA_VERSION "0.10.0" #define COMMA_VERSION "0.9.10"
+3
View File
@@ -2,6 +2,7 @@ import contextlib
import gc import gc
import os import os
import pytest import pytest
import random
from openpilot.common.prefix import OpenpilotPrefix from openpilot.common.prefix import OpenpilotPrefix
from openpilot.system.manager import manager from openpilot.system.manager import manager
@@ -48,6 +49,8 @@ def clean_env():
@pytest.fixture(scope="function", autouse=True) @pytest.fixture(scope="function", autouse=True)
def openpilot_function_fixture(request): def openpilot_function_fixture(request):
random.seed(0)
with clean_env(): with clean_env():
# setup a clean environment for each test # setup a clean environment for each test
with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix:
+1 -1
View File
@@ -187,7 +187,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>||| |Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>|||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>||| |Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>|||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-24">Buy Here</a></sub></details>||| |Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-24">Buy Here</a></sub></details>|||
|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=LC 2024-25">Buy Here</a></sub></details>||| |Lexus|LC 2024|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=LC 2024">Buy Here</a></sub></details>|||
|Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>||| |Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>|||
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>||| |Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>|||
|Lexus|NX Hybrid 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX Hybrid 2018-19">Buy Here</a></sub></details>||| |Lexus|NX Hybrid 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX Hybrid 2018-19">Buy Here</a></sub></details>|||
+5
View File
@@ -81,9 +81,11 @@ docs = [
] ]
testing = [ testing = [
"coverage",
"hypothesis ==6.47.*", "hypothesis ==6.47.*",
"mypy", "mypy",
"pytest", "pytest",
"pytest-cov",
"pytest-cpp", "pytest-cpp",
"pytest-subtests", "pytest-subtests",
# https://github.com/pytest-dev/pytest-xdist/issues/1215 # https://github.com/pytest-dev/pytest-xdist/issues/1215
@@ -264,5 +266,8 @@ lint.flake8-implicit-str-concat.allow-multiline = false
"unittest".msg = "Use pytest" "unittest".msg = "Use pytest"
"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" "pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure"
[tool.coverage.run]
concurrency = ["multiprocessing", "thread"]
[tool.ruff.format] [tool.ruff.format]
quote-style = "preserve" quote-style = "preserve"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# run_openpilot_docker.sh
# POSIX-compliant script to run openpilot in Docker for local testing
# === Configurable Variables ===
# Base image to use (required)
BASE_IMAGE="${BASE_IMAGE:-commaai/openpilot-base:latest}"
# Working directory inside the container
WORKDIR="/tmp/openpilot"
# Local project path
LOCAL_DIR="$PWD"
# Shared memory size (adjust for large builds/tests)
SHM_SIZE="2G"
# Environment configuration
CI=1
PYTHONWARNINGS="error"
FILEREADER_CACHE=1
PYTHONPATH="$WORKDIR"
# Optional: GitHub Actions env vars — set them only if needed for local mirroring/debug
USE_GITHUB_ENV_VARS=false # set to true to enable GitHub-related mounts/envs
GITHUB_WORKSPACE="${GITHUB_WORKSPACE:-$HOME/openpilot_ci}" # fallback path
# === Docker Command ===
docker run --rm \
--shm-size "$SHM_SIZE" \
-v "$LOCAL_DIR":"$WORKDIR" \
-w "$WORKDIR" \
-e CI="$CI" \
-e PYTHONWARNINGS="$PYTHONWARNINGS" \
-e FILEREADER_CACHE="$FILEREADER_CACHE" \
-e PYTHONPATH="$PYTHONPATH" \
${USE_GITHUB_ENV_VARS:+\
-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 "${1:-/bin/bash}"
+23 -21
View File
@@ -5,7 +5,6 @@ set -e
DEFAULT_REPO_URL="https://github.com/sunnypilot" DEFAULT_REPO_URL="https://github.com/sunnypilot"
START_AT_BOOT=false START_AT_BOOT=false
RESTORE_MODE=false RESTORE_MODE=false
RUNNER_VERSION="2.325.0"
# Parse command line arguments # Parse command line arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -109,9 +108,9 @@ setup_system_configs() {
install_runner() { install_runner() {
echo "Downloading and setting up runner..." echo "Downloading and setting up runner..."
cd "$RUNNER_DIR" cd "$RUNNER_DIR"
curl -o actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz curl -o actions-runner-linux-arm64-2.322.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.322.0/actions-runner-linux-arm64-2.322.0.tar.gz
sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-2.322.0.tar.gz
sudo rm ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz sudo rm ./actions-runner-linux-arm64-2.322.0.tar.gz
sudo chmod +x ./config.sh sudo chmod +x ./config.sh
} }
@@ -150,32 +149,25 @@ EOL
} }
install_service() { install_service() {
local service_name
if [ -f "${RUNNER_DIR}/.service" ]; then
service_name=$(cat "${RUNNER_DIR}/.service")
else
service_name="actions.runner.sunnypilot.$(uname -n)"
fi
create_service_template
remount_rw remount_rw
local service_path="/etc/systemd/system/${service_name}"
echo "Installing systemd service..." echo "Installing systemd service..."
if [ -f "${service_path}" ]; then
echo "Service ${service_path} found in systemd, we will delete it"
sudo rm -f "${service_path}"
fi
cd "$RUNNER_DIR" cd "$RUNNER_DIR"
sudo ./svc.sh install $RUNNER_USER sudo ./svc.sh install $RUNNER_USER
if [ "$START_AT_BOOT" = false ]; then if [ "$START_AT_BOOT" = false ]; then
local service_name
if [ -f "${RUNNER_DIR}/.service" ]; then
service_name=$(cat "${RUNNER_DIR}/.service")
else
service_name="actions.runner.sunnypilot.$(uname -n)"
fi
sudo systemctl disable "${service_name}" sudo systemctl disable "${service_name}"
fi fi
remount_ro remount_ro
} }
check_restore_prerequisites() { check_restore_prerequisites() {
local needs_restore=false
local can_restore=false local can_restore=false
local service_name="" local service_name=""
@@ -197,16 +189,25 @@ check_restore_prerequisites() {
exit 1 exit 1
fi fi
# Then check if restoration is needed (if either service or user is missing)
if ! systemctl list-unit-files "${service_name}" &>/dev/null; then
echo "Service ${service_name} not found in systemd"
needs_restore=true
fi
if ! id "${RUNNER_USER}" &>/dev/null; then if ! id "${RUNNER_USER}" &>/dev/null; then
echo "User ${RUNNER_USER} does not exist" echo "User ${RUNNER_USER} does not exist"
needs_restore=true
fi fi
# Only proceed if we can restore AND need to restore # Only proceed if we can restore AND need to restore
if [ "$can_restore" = true ]; then if [ "$can_restore" = true ] && [ "$needs_restore" = true ]; then
echo "Restoration is possible" echo "Restoration is needed and possible"
return 0 return 0
else else
echo "No restoration possible" if [ "$needs_restore" = false ]; then
echo "System is already properly configured (user and service exist)"
fi
exit 0 exit 0
fi fi
} }
@@ -225,6 +226,7 @@ perform_install() {
setup_system_configs setup_system_configs
install_runner install_runner
set_directory_permissions set_directory_permissions
create_service_template
configure_runner configure_runner
install_service install_service
echo "Installation completed successfully" echo "Installation completed successfully"
+12 -27
View File
@@ -13,32 +13,17 @@ def create_short_name(full_name):
words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)] words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)]
if len(words) == 1: if len(words) == 1:
return words[0][:8].upper() # If there's only one word, return it as is, lowercased, truncated to 8 characters
truncated = words[0][:8]
return truncated.lower() if truncated.isupper() else truncated
# Handle special case: Name + Version (e.g., "Word A1" -> "WordA1") # Handle special case: Name + Version (e.g., "Word A1" -> "WordA1")
if len(words) == 2 and re.match(r'^[A-Za-z]\d+$', words[1]): if len(words) == 2 and re.match(r'^[A-Za-z]\d+$', words[1]):
return (words[0] + words[1])[:8].upper() first_word = words[0].lower() if words[0].isupper() else words[0]
return (first_word + words[1])[:8]
result = "" # Normal case: first letter and trailing numbers from each word
for word in words: result = ''.join(word if word.isdigit() else word[0] + (re.search(r'\d+$', word) or [''])[0] for word in words)
# Version or number patterns
if (re.match(r'^\d+[a-zA-Z]+$', word) or
re.match(r'^\d+[vVbB]\d+$', word) or
re.match(r'^[vVbB]\d+$', word) or
re.match(r'^\d{4}$', word)):
result += word.upper()
# All uppercase abbreviations (2-3 letters)
elif re.match(r'^[A-Z]{2,3}$', word):
result += word
# Letters+digits (for example tr15 rev2)
elif re.match(r'^[a-zA-Z]+[0-9]+$', word):
result += word[0].upper() + ''.join(re.findall(r'\d+', word))
elif word.isalpha():
result += word[0].upper()
elif word.isdigit():
result += word
else:
result += word[0].upper()
return result[:8] return result[:8]
@@ -73,14 +58,14 @@ def generate_metadata(model_path: Path, output_dir: Path, short_name: str):
"artifact": { "artifact": {
"file_name": tinygrad_file.name, "file_name": tinygrad_file.name,
"download_uri": { "download_uri": {
"url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/<FILLME>",
"sha256": tinygrad_hash "sha256": tinygrad_hash
} }
}, },
"metadata": { "metadata": {
"file_name": metadata_file.name, "file_name": metadata_file.name,
"download_uri": { "download_uri": {
"url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/<FILLME>",
"sha256": metadata_hash "sha256": metadata_hash
} }
} }
@@ -98,12 +83,12 @@ def create_metadata_json(models: list, output_dir: Path, custom_name=None, short
"ref": upstream_branch, "ref": upstream_branch,
"environment": "development", "environment": "development",
"runner": "tinygrad", "runner": "tinygrad",
"build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
"models": models,
"overrides": {},
"index": -1, "index": -1,
"minimum_selector_version": "-1", "minimum_selector_version": "-1",
"generation": "-1", "generation": "-1",
"build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
"overrides": {},
"models": models,
} }
# Write metadata to output_dir # Write metadata to output_dir
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9fc1f7f31d41f26ea7d6f52b3096f7a91844a3b897bc233a8489253c46f0403b
size 6324
+1 -2
View File
@@ -93,8 +93,7 @@ class Controls(ControlsExt):
torque_params.frictionCoefficientFiltered) torque_params.frictionCoefficientFiltered)
self.LaC.extension.update_model_v2(self.sm['modelV2']) self.LaC.extension.update_model_v2(self.sm['modelV2'])
calculated_lag = self.LaC.extension.lagd_torqued_main(self.CP, self.sm['liveDelay']) self.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay)
self.LaC.extension.update_lateral_lag(calculated_lag)
long_plan = self.sm['longitudinalPlan'] long_plan = self.sm['longitudinalPlan']
model_v2 = self.sm['modelV2'] model_v2 = self.sm['modelV2']
+9
View File
@@ -1,4 +1,5 @@
import numpy as np import numpy as np
from cereal import log
from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.realtime import DT_CTRL, DT_MDL from openpilot.common.realtime import DT_CTRL, DT_MDL
@@ -39,6 +40,14 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll):
return float(new_curvature), limited_accel or limited_max_curv return float(new_curvature), limited_accel or limited_max_curv
def get_speed_error(modelV2: log.ModelDataV2, v_ego: float) -> float:
# ToDo: Try relative error, and absolute speed
if len(modelV2.temporalPose.trans):
vel_err = np.clip(modelV2.temporalPose.trans[0] - v_ego, -MAX_VEL_ERR, MAX_VEL_ERR)
return float(vel_err)
return 0.0
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.05): def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.05):
if len(speeds) == len(t_idxs): if len(speeds) == len(t_idxs):
v_now = speeds[0] v_now = speeds[0]
+10 -2
View File
@@ -31,6 +31,7 @@ class LatControlTorque(LatControl):
self.pid = PIDController(self.torque_params.kp, self.torque_params.ki, self.pid = PIDController(self.torque_params.kp, self.torque_params.ki,
k_f=self.torque_params.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max) k_f=self.torque_params.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max)
self.torque_from_lateral_accel = CI.torque_from_lateral_accel() self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
self.use_steering_angle = self.torque_params.useSteeringAngle
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
self.extension = LatControlTorqueExt(self, CP, CP_SP) self.extension = LatControlTorqueExt(self, CP, CP_SP)
@@ -46,9 +47,16 @@ class LatControlTorque(LatControl):
output_torque = 0.0 output_torque = 0.0
pid_log.active = False pid_log.active = False
else: else:
actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) actual_curvature_vm = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) if self.use_steering_angle:
actual_curvature = actual_curvature_vm
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
else:
assert calibrated_pose is not None
actual_curvature_pose = calibrated_pose.angular_velocity.yaw / CS.vEgo
actual_curvature = np.interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_pose])
curvature_deadzone = 0.0
desired_lateral_accel = desired_curvature * CS.vEgo ** 2 desired_lateral_accel = desired_curvature * CS.vEgo ** 2
# desired rate is the desired rate of change in the setpoint, not the absolute desired curvature # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature
+9 -11
View File
@@ -11,7 +11,7 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_speed_error, get_accel_from_plan
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
@@ -54,7 +54,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
self.CP = CP self.CP = CP
self.mpc = LongitudinalMpc(dt=dt) self.mpc = LongitudinalMpc(dt=dt)
# TODO remove mpc modes when TR released
self.mpc.mode = 'acc' self.mpc.mode = 'acc'
LongitudinalPlannerSP.__init__(self, self.CP, self.mpc) LongitudinalPlannerSP.__init__(self, self.CP, self.mpc)
self.fcw = False self.fcw = False
@@ -64,6 +63,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.a_desired = init_a self.a_desired = init_a
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt) self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
self.prev_accel_clip = [ACCEL_MIN, ACCEL_MAX] self.prev_accel_clip = [ACCEL_MIN, ACCEL_MAX]
self.v_model_error = 0.0
self.output_a_target = 0.0 self.output_a_target = 0.0
self.output_should_stop = False self.output_should_stop = False
@@ -73,12 +73,12 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.solverExecutionTime = 0.0 self.solverExecutionTime = 0.0
@staticmethod @staticmethod
def parse_model(model_msg): def parse_model(model_msg, model_error):
if (len(model_msg.position.x) == ModelConstants.IDX_N and if (len(model_msg.position.x) == ModelConstants.IDX_N and
len(model_msg.velocity.x) == ModelConstants.IDX_N and len(model_msg.velocity.x) == ModelConstants.IDX_N and
len(model_msg.acceleration.x) == ModelConstants.IDX_N): len(model_msg.acceleration.x) == ModelConstants.IDX_N):
x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x) x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x) - model_error * T_IDXS_MPC
v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x) v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x) - model_error
a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.acceleration.x) a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.acceleration.x)
j = np.zeros(len(T_IDXS_MPC)) j = np.zeros(len(T_IDXS_MPC))
else: else:
@@ -94,13 +94,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
def update(self, sm): def update(self, sm):
self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
if not self.mlsim:
self.mpc.mode = self.mode
LongitudinalPlannerSP.update(self, sm) LongitudinalPlannerSP.update(self, sm)
if dec_mpc_mode := self.get_mpc_mode(): if dec_mpc_mode := self.get_mpc_mode():
self.mode = dec_mpc_mode self.mode = dec_mpc_mode
if not self.mlsim:
self.mpc.mode = dec_mpc_mode
if len(sm['carControl'].orientationNED) == 3: if len(sm['carControl'].orientationNED) == 3:
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
@@ -137,7 +133,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
# Prevent divergence, smooth in current v_ego # Prevent divergence, smooth in current v_ego
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
x, v, a, j, throttle_prob = self.parse_model(sm['modelV2']) # Compute model v_ego error
self.v_model_error = get_speed_error(sm['modelV2'], v_ego)
x, v, a, j, throttle_prob = self.parse_model(sm['modelV2'], self.v_model_error)
# Don't clip at low speeds since throttle_prob doesn't account for creep # Don't clip at low speeds since throttle_prob doesn't account for creep
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
@@ -173,7 +171,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop output_should_stop_e2e = sm['modelV2'].action.shouldStop
if self.mode == 'acc' or not self.mlsim: if self.mode == 'acc':
output_a_target = output_a_target_mpc output_a_target = output_a_target_mpc
self.output_should_stop = output_should_stop_mpc self.output_should_stop = output_should_stop_mpc
else: else:
+8 -3
View File
@@ -157,7 +157,8 @@ class LateralLagEstimator:
block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE, block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE,
window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC, window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC,
min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC, min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC,
max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE): max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE,
enabled: bool = True):
self.dt = dt self.dt = dt
self.window_sec = window_sec self.window_sec = window_sec
self.okay_window_sec = okay_window_sec self.okay_window_sec = okay_window_sec
@@ -172,6 +173,7 @@ class LateralLagEstimator:
self.min_confidence = min_confidence self.min_confidence = min_confidence
self.max_lat_accel = max_lat_accel self.max_lat_accel = max_lat_accel
self.max_lat_accel_diff = max_lat_accel_diff self.max_lat_accel_diff = max_lat_accel_diff
self.enabled = enabled
self.t = 0.0 self.t = 0.0
self.lat_active = False self.lat_active = False
@@ -206,7 +208,7 @@ class LateralLagEstimator:
liveDelay = msg.liveDelay liveDelay = msg.liveDelay
valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get() valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get()
if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): if self.enabled and self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std):
if valid_std > MAX_LAG_STD: if valid_std > MAX_LAG_STD:
liveDelay.status = log.LiveDelayData.Status.invalid liveDelay.status = log.LiveDelayData.Status.invalid
else: else:
@@ -369,7 +371,10 @@ def main():
params = Params() params = Params()
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency) # TODO: remove me, lagd is in shadow mode on release
is_release = params.get_bool("IsReleaseBranch")
lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency, enabled=not is_release)
if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None: if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None:
lag, valid_blocks = initial_lag_params lag, valid_blocks = initial_lag_params
lag_learner.reset(lag, valid_blocks) lag_learner.reset(lag, valid_blocks)
+13
View File
@@ -110,6 +110,19 @@ class TestLagd:
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
assert msg.liveDelay.calPerc == 100 assert msg.liveDelay.calPerc == 100
def test_disabled_estimator(self):
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, enabled=False)
lag_frames = 5
process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE)
msg = estimator.get_msg(True)
assert msg.liveDelay.status == 'unestimated'
assert np.allclose(msg.liveDelay.lateralDelay, 1.0, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
assert msg.liveDelay.calPerc == 100
def test_estimator_masking(self): def test_estimator_masking(self):
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19) mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1)
+2 -6
View File
@@ -11,8 +11,6 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
HISTORY = 5 # secs HISTORY = 5 # secs
POINTS_PER_BUCKET = 1500 POINTS_PER_BUCKET = 1500
MIN_POINTS_TOTAL = 4000 MIN_POINTS_TOTAL = 4000
@@ -51,10 +49,8 @@ class TorqueBuckets(PointBuckets):
break break
class TorqueEstimator(ParameterEstimator, LagdToggle): class TorqueEstimator(ParameterEstimator):
def __init__(self, CP, decimated=False, track_all_points=False): def __init__(self, CP, decimated=False, track_all_points=False):
super().__init__()
self.CP = CP
self.hist_len = int(HISTORY / DT_MDL) self.hist_len = int(HISTORY / DT_MDL)
self.lag = 0.0 self.lag = 0.0
self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters
@@ -180,7 +176,7 @@ class TorqueEstimator(ParameterEstimator, LagdToggle):
elif which == "liveCalibration": elif which == "liveCalibration":
self.calibrator.feed_live_calib(msg) self.calibrator.feed_live_calib(msg)
elif which == "liveDelay": elif which == "liveDelay":
self.lag = self.lagd_torqued_main(self.CP, msg) self.lag = msg.lateralDelay
# calculate lateral accel from past steering torque # calculate lateral accel from past steering torque
elif which == "livePose": elif which == "livePose":
if len(self.raw_points['steer_torque']) == self.hist_len: if len(self.raw_points['steer_torque']) == self.hist_len:
+14 -12
View File
@@ -56,15 +56,17 @@ for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
tg_compile(flags, model_name) tg_compile(flags, model_name)
# Compile BIG model if USB GPU is available # Compile BIG model if USB GPU is available
if "USBGPU" in os.environ: import subprocess
import subprocess from tinygrad import Device
# because tg doesn't support multi-process
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath) # because tg doesn't support multi-process
if b"AMD" in devs: devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath)
print("USB GPU detected... building") if b"AMD" in devs:
flags = "AMD=1 AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0" del Device
bp = tg_compile(flags, "big_driving_policy") print("USB GPU detected... building")
bv = tg_compile(flags, "big_driving_vision") flags = "AMD=1 AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0"
lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially bp = tg_compile(flags, "big_driving_policy")
else: bv = tg_compile(flags, "big_driving_vision")
print("USB GPU not detected... skipping") lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially
else:
print("USB GPU not detected... skipping")
+10 -1
View File
@@ -14,6 +14,7 @@ import pickle
import ctypes import ctypes
import numpy as np import numpy as np
from pathlib import Path from pathlib import Path
from setproctitle import setproctitle
from cereal import messaging from cereal import messaging
from cereal.messaging import PubMaster, SubMaster from cereal.messaging import PubMaster, SubMaster
@@ -24,6 +25,7 @@ from openpilot.common.transformations.model import dmonitoringmodel_intrinsics,
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.selfdrive.modeld.models.commonmodel_pyx import CLContext, MonitoringModelFrame from openpilot.selfdrive.modeld.models.commonmodel_pyx import CLContext, MonitoringModelFrame
from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid
from openpilot.system import sentry
MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE
CALIB_LEN = 3 CALIB_LEN = 3
@@ -131,8 +133,12 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts:
def main(): def main():
setproctitle(PROCESS_NAME)
config_realtime_process(7, 5) config_realtime_process(7, 5)
sentry.set_tag("daemon", PROCESS_NAME)
cloudlog.bind(daemon=PROCESS_NAME)
cl_context = CLContext() cl_context = CLContext()
model = ModelState(cl_context) model = ModelState(cl_context)
cloudlog.warning("models loaded, dmonitoringmodeld starting") cloudlog.warning("models loaded, dmonitoringmodeld starting")
@@ -174,4 +180,7 @@ if __name__ == "__main__":
try: try:
main() main()
except KeyboardInterrupt: except KeyboardInterrupt:
cloudlog.warning("got SIGINT") cloudlog.warning(f"child {PROCESS_NAME} got SIGINT")
except Exception:
sentry.capture_exception()
raise
+7
View File
@@ -89,6 +89,13 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T)
fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T)
# temporal pose
temporal_pose = modelV2.temporalPose
temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist()
temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist()
temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist()
temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist()
# poly path # poly path
fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T)
+15 -16
View File
@@ -19,6 +19,7 @@ import numpy as np
import cereal.messaging as messaging import cereal.messaging as messaging
from cereal import car, log from cereal import car, log
from pathlib import Path from pathlib import Path
from setproctitle import setproctitle
from cereal.messaging import PubMaster, SubMaster from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from opendbc.car.car_helpers import get_demo_car_params from opendbc.car.car_helpers import get_demo_car_params
@@ -28,15 +29,14 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix from openpilot.common.transformations.model import get_warp_matrix
from openpilot.system import sentry
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
PROCESS_NAME = "selfdrive.modeld.modeld" PROCESS_NAME = "selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -46,8 +46,8 @@ POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl'
VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl' VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl'
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl' POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
LAT_SMOOTH_SECONDS = 0.1 LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.3 LONG_SMOOTH_SECONDS = 0.0
MIN_LAT_CONTROL_SPEED = 0.3 MIN_LAT_CONTROL_SPEED = 0.3
@@ -60,11 +60,7 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.
action_t=long_action_t) action_t=long_action_t)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS)
desired_curvature = get_curvature_from_plan(plan[:,Plan.T_FROM_CURRENT_EULER][:,2], desired_curvature = model_output['desired_curvature'][0, 0]
plan[:,Plan.ORIENTATION_RATE][:,2],
ModelConstants.T_IDXS,
v_ego,
lat_action_t)
if v_ego > MIN_LAT_CONTROL_SPEED: if v_ego > MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS) desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS)
else: else:
@@ -90,7 +86,6 @@ class ModelState:
prev_desire: np.ndarray # for tracking the rising edge of the pulse prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self, context: CLContext): def __init__(self, context: CLContext):
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
with open(VISION_METADATA_PATH, 'rb') as f: with open(VISION_METADATA_PATH, 'rb') as f:
vision_metadata = pickle.load(f) vision_metadata = pickle.load(f)
self.vision_input_shapes = vision_metadata['input_shapes'] self.vision_input_shapes = vision_metadata['input_shapes']
@@ -179,7 +174,7 @@ class ModelState:
# TODO model only uses last value now # TODO model only uses last value now
self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:]
self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :] self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :]
self.numpy_inputs['prev_desired_curv'][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs] self.numpy_inputs['prev_desired_curv'][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
if SEND_RAW_PRED: if SEND_RAW_PRED:
@@ -191,6 +186,9 @@ class ModelState:
def main(demo=False): def main(demo=False):
cloudlog.warning("modeld init") cloudlog.warning("modeld init")
sentry.set_tag("daemon", PROCESS_NAME)
cloudlog.bind(daemon=PROCESS_NAME)
setproctitle(PROCESS_NAME)
if not USBGPU: if not USBGPU:
# USB GPU currently saturates a core so can't do this yet, # USB GPU currently saturates a core so can't do this yet,
# also need to move the aux USB interrupts for good timings # also need to move the aux USB interrupts for good timings
@@ -253,8 +251,6 @@ def main(demo=False):
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
cloudlog.info("modeld got CarParams: %s", CP.brand) cloudlog.info("modeld got CarParams: %s", CP.brand)
modeld_lagd = LagdToggle()
# TODO this needs more thought, use .2s extra for now to estimate other delays # TODO this needs more thought, use .2s extra for now to estimate other delays
# TODO Move smooth seconds to action function # TODO Move smooth seconds to action function
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
@@ -300,7 +296,7 @@ def main(demo=False):
is_rhd = sm["driverMonitoringState"].isRHD is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["roadCameraState"].frameId frame_id = sm["roadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.) v_ego = max(sm["carState"].vEgo, 0.)
lat_delay = modeld_lagd.lagd_main(CP, sm, model) lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32) lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32)
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
@@ -378,4 +374,7 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
main(demo=args.demo) main(demo=args.demo)
except KeyboardInterrupt: except KeyboardInterrupt:
cloudlog.warning("got SIGINT") cloudlog.warning(f"child {PROCESS_NAME} got SIGINT")
except Exception:
sentry.capture_exception()
raise
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1741cad23f6f451782b5db6182218749ee12072e393d57eac36d8d5c55d9358a oid sha256:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d
size 15583374 size 15971805
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3d2bd82ba42341dba1bda5426e45c4c646db604c9ac422156eaa2b9ef26194f9 oid sha256:3ac4867fbc618037e8d03143edbfeeae960f2025644b5dcf36c6665271b4f874
size 46265993 size 34883375
+7 -6
View File
@@ -88,12 +88,6 @@ class Parser:
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
for k in ['lead_prob', 'lane_lines_prob']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
self.parse_binary_crossentropy('meta', outs) self.parse_binary_crossentropy('meta', outs)
return outs return outs
@@ -101,10 +95,17 @@ class Parser:
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
if 'lat_planner_solution' in outs: if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs: if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
return outs return outs
+1 -1
View File
@@ -1 +1 @@
de16c6fbe14e121c5e74cd944ce03a0e4672540d f440c9e0469d32d350aa99ddaa8f44591a2ce690
+2 -2
View File
@@ -7,7 +7,7 @@ from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.selfdrive.ui.layouts.network import NetworkLayout from openpilot.selfdrive.ui.layouts.network import NetworkLayout
from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.lib.widget import Widget
@@ -132,7 +132,7 @@ class SettingsLayout(Widget):
if panel.instance: if panel.instance:
panel.instance.render(content_rect) panel.instance.render(content_rect)
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
# Check close button # Check close button
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect): if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
if self._close_callback: if self._close_callback:
-7
View File
@@ -22,7 +22,6 @@ DESCRIPTIONS = {
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.", "AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", 'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
"IsMetric": "Display speed in km/h instead of mph.", "IsMetric": "Display speed in km/h instead of mph.",
"RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.",
} }
@@ -75,12 +74,6 @@ class TogglesLayout(Widget):
self._params.get_bool("RecordFront"), self._params.get_bool("RecordFront"),
icon="monitoring.png", icon="monitoring.png",
), ),
toggle_item(
"Record Microphone Audio",
DESCRIPTIONS["RecordAudio"],
self._params.get_bool("RecordAudio"),
icon="microphone.png",
),
toggle_item( toggle_item(
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png" "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png"
), ),
+2 -2
View File
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from collections.abc import Callable from collections.abc import Callable
from cereal import log from cereal import log
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.lib.widget import Widget
@@ -135,7 +135,7 @@ class Sidebar(Widget):
else: else:
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD) self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
def _handle_mouse_release(self, mouse_pos: MousePos): def _handle_mouse_release(self, mouse_pos: rl.Vector2):
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN): if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
if self._on_settings_click: if self._on_settings_click:
self._on_settings_click() self._on_settings_click()
@@ -45,6 +45,17 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
}); });
addItem(experimentalLongitudinalToggle); addItem(experimentalLongitudinalToggle);
enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), "");
addItem(enableGithubRunner);
// error log button
errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."));
connect(errorLogBtn, &ButtonControl::clicked, [=]() {
std::string txt = util::read_file("/data/community/crashes/error.log");
ConfirmationDialog::rich(QString::fromStdString(txt), this);
});
addItem(errorLogBtn);
// Joystick and longitudinal maneuvers should be hidden on release branches // Joystick and longitudinal maneuvers should be hidden on release branches
is_release = params.getBool("IsReleaseBranch"); is_release = params.getBool("IsReleaseBranch");
@@ -93,6 +104,8 @@ void DeveloperPanel::updateToggles(bool _offroad) {
experimentalLongitudinalToggle->refresh(); experimentalLongitudinalToggle->refresh();
// Handle specific controls visibility for release branches // Handle specific controls visibility for release branches
enableGithubRunner->setVisible(!is_release);
errorLogBtn->setVisible(!is_release);
joystickToggle->setVisible(!is_release); joystickToggle->setVisible(!is_release);
offroad = _offroad; offroad = _offroad;
@@ -16,8 +16,10 @@ private:
Params params; Params params;
ParamControl* adbToggle; ParamControl* adbToggle;
ParamControl* joystickToggle; ParamControl* joystickToggle;
ButtonControl* errorLogBtn;
ParamControl* longManeuverToggle; ParamControl* longManeuverToggle;
ParamControl* experimentalLongitudinalToggle; ParamControl* experimentalLongitudinalToggle;
ParamControl* enableGithubRunner;
bool is_release; bool is_release;
bool offroad = false; bool offroad = false;
+21 -23
View File
@@ -68,13 +68,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
"../assets/icons/monitoring.png", "../assets/icons/monitoring.png",
true, true,
}, },
{
"RecordAudio",
tr("Record and Upload Microphone Audio"),
tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
"../assets/icons/microphone.png",
true,
},
{ {
"IsMetric", "IsMetric",
tr("Use Metric System"), tr("Use Metric System"),
@@ -349,21 +342,25 @@ void DevicePanel::updateCalibDescription() {
} }
} }
int lag_perc = 0; const bool is_release = params.getBool("IsReleaseBranch");
std::string lag_bytes = params.get("LiveDelay"); if (!is_release) {
if (!lag_bytes.empty()) { int lag_perc = 0;
try { std::string lag_bytes = params.get("LiveDelay");
AlignedBuffer aligned_buf; if (!lag_bytes.empty()) {
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); try {
lag_perc = cmsg.getRoot<cereal::Event>().getLiveDelay().getCalPerc(); AlignedBuffer aligned_buf;
} catch (kj::Exception) { capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size()));
qInfo() << "invalid LiveDelay"; lag_perc = cmsg.getRoot<cereal::Event>().getLiveDelay().getCalPerc();
} catch (kj::Exception) {
qInfo() << "invalid LiveDelay";
}
}
desc += "\n\n";
if (lag_perc < 100) {
desc += tr("Steering lag calibration is %1% complete.").arg(lag_perc);
} else {
desc += tr("Steering lag calibration is complete.");
} }
}
if (lag_perc < 100) {
desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc);
} else {
desc += tr("\n\nSteering lag calibration is complete.");
} }
std::string torque_bytes = params.get("LiveTorqueParameters"); std::string torque_bytes = params.get("LiveTorqueParameters");
@@ -375,10 +372,11 @@ void DevicePanel::updateCalibDescription() {
// don't add for non-torque cars // don't add for non-torque cars
if (torque.getUseParams()) { if (torque.getUseParams()) {
int torque_perc = torque.getCalPerc(); int torque_perc = torque.getCalPerc();
desc += is_release ? "\n\n" : " ";
if (torque_perc < 100) { if (torque_perc < 100) {
desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); desc += tr("Steering torque response calibration is %1% complete.").arg(torque_perc);
} else { } else {
desc += tr(" Steering torque response calibration is complete."); desc += tr("Steering torque response calibration is complete.");
} }
} }
} catch (kj::Exception) { } catch (kj::Exception) {
-168
View File
@@ -34,7 +34,6 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
drawLead(painter, lead_two, lead_vertices[1], surface_rect); drawLead(painter, lead_two, lead_vertices[1], surface_rect);
} }
} }
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
painter.restore(); painter.restore();
} }
@@ -174,173 +173,6 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
(1 - t) * start.alphaF() + t * end.alphaF()); (1 - t) * start.alphaF() + t * end.alphaF());
} }
void ModelRenderer::drawLeadStatus(QPainter &painter, int height, int width) {
auto *s = uiState();
auto &sm = *(s->sm);
if (!sm.alive("radarState")) return;
const auto &radar_state = sm["radarState"].getRadarState();
const auto &lead_one = radar_state.getLeadOne();
const auto &lead_two = radar_state.getLeadTwo();
// Check if we have any active leads
bool has_lead_one = lead_one.getStatus();
bool has_lead_two = lead_two.getStatus();
if (!has_lead_one && !has_lead_two) {
// Fade out status display
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
if (lead_status_alpha <= 0.0f) return;
} else {
// Fade in status display
lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f);
}
// Draw status for each lead vehicle under its chevron
if (true) {
drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1");
}
if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) {
drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2");
}
}
void ModelRenderer::drawLeadStatusAtPosition(QPainter &painter,
const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos,
int height, int width,
const QString &label) {
float d_rel = lead_data.getDRel();
float v_rel = lead_data.getVRel();
auto *s = uiState();
auto &sm = *(s->sm);
float v_ego = sm["carState"].getCarState().getVEgo();
int chevron_data = std::atoi(Params().get("ChevronInfo").c_str());
// Calculate chevron size (same logic as drawLead)
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35;
QFont content_font = painter.font();
content_font.setPixelSize(35);
content_font.setBold(true);
painter.setFont(content_font);
QFontMetrics fm(content_font);
bool is_metric = s->scene.is_metric;
QStringList text_lines;
const int chevron_types = 3;
const int chevron_all = chevron_types + 1; // All metrics (value 4)
QStringList chevron_text[chevron_types];
int position;
float val;
// Distance display (chevron_data == 1 or all)
if (chevron_data == 1 || chevron_data == chevron_all) {
position = 0;
val = std::max(0.0f, d_rel);
QString distance_unit = is_metric ? "m" : "ft";
if (!is_metric) {
val *= 3.28084f; // Convert meters to feet
}
chevron_text[position].append(QString::number(val, 'f', 0) + " " + distance_unit);
}
// Absolute velocity display (chevron_data == 2 or all)
if (chevron_data == 2 || chevron_data == chevron_all) {
position = (chevron_data == 2) ? 0 : 1;
val = std::max(0.0f, (v_rel + v_ego) * (is_metric ? static_cast<float>(MS_TO_KPH) : static_cast<float>(MS_TO_MPH)));
chevron_text[position].append(QString::number(val, 'f', 0) + " " + (is_metric ? "km/h" : "mph"));
}
// Time-to-contact display (chevron_data == 3 or all)
if (chevron_data == 3 || chevron_data == chevron_all) {
position = (chevron_data == 3) ? 0 : 2;
val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f;
QString ttc_str = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---";
chevron_text[position].append(ttc_str);
}
// Collect all non-empty text lines
for (int i = 0; i < chevron_types; ++i) {
if (!chevron_text[i].isEmpty()) {
text_lines.append(chevron_text[i]);
}
}
// If no text to display, return early
if (text_lines.isEmpty()) {
return;
}
// Text box dimensions
float str_w = 150; // Width of text area
float str_h = 45; // Height per line
// Position text below chevron, centered horizontally
float text_x = chevron_pos.x() - str_w / 2;
float text_y = chevron_pos.y() + sz + 15;
// Clamp to screen bounds
text_x = std::clamp(text_x, 10.0f, (float)width - str_w - 10);
// Shadow offset
QPoint shadow_offset(2, 2);
// Draw each line of text with shadow
for (int i = 0; i < text_lines.size(); ++i) {
if (!text_lines[i].isEmpty()) {
QRect textRect(text_x, text_y + (i * str_h), str_w, str_h);
// Draw shadow
painter.setPen(QColor(0x0, 0x0, 0x0, (int)(200 * lead_status_alpha)));
painter.drawText(textRect.translated(shadow_offset.x(), shadow_offset.y()),
Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]);
// Determine text color based on content and danger level
QColor text_color;
// Check if this is a distance line (contains 'm' or 'ft')
if (text_lines[i].contains("m") || text_lines[i].contains("ft")) {
if (d_rel < 20.0f) {
text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - danger
} else if (d_rel < 40.0f) {
text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution
} else {
text_color = QColor(80, 255, 120, (int)(255 * lead_status_alpha)); // Green - safe
}
}
// Enhanced color coding for time-to-contact
else if (text_lines[i].contains("s") && !text_lines[i].contains("---")) {
float ttc_val = text_lines[i].left(text_lines[i].length() - 1).toFloat();
if (ttc_val < 3.0f) {
text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - urgent
} else if (ttc_val < 6.0f) {
text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution
} else {
text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White - safe
}
}
else {
text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White for other lines
}
// Draw main text
painter.setPen(text_color);
painter.drawText(textRect, Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]);
}
}
// Reset pen
painter.setPen(Qt::NoPen);
}
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &vd, const QRect &surface_rect) { const QPointF &vd, const QRect &surface_rect) {
const float speedBuff = 10.; const float speedBuff = 10.;
-11
View File
@@ -34,12 +34,6 @@ protected:
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out); bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off, void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
QPolygonF *pvd, int max_idx, bool allow_invert = true); QPolygonF *pvd, int max_idx, bool allow_invert = true);
void drawLeadStatus(QPainter &painter, int height, int width);
void drawLeadStatusAtPosition(QPainter &painter,
const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos,
int height, int width,
const QString &label);
void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect); void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect);
void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line); void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line);
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
@@ -64,9 +58,4 @@ protected:
QPointF lead_vertices[2] = {}; QPointF lead_vertices[2] = {};
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero(); Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
QRectF clip_region; QRectF clip_region;
float lead_status_alpha = 0.0f;
QPointF lead_status_pos;
QString lead_status_text;
QColor lead_status_color;
}; };
+3 -19
View File
@@ -24,11 +24,10 @@ void Sidebar::drawMetric(QPainter &p, const QPair<QString, QString> &label, QCol
p.drawText(rect.adjusted(22, 0, 0, 0), Qt::AlignCenter, label.first + "\n" + label.second); p.drawText(rect.adjusted(22, 0, 0, 0), Qt::AlignCenter, label.first + "\n" + label.second);
} }
Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false), mic_indicator_pressed(false) { Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false) {
home_img = loadPixmap("../assets/images/button_home.png", home_btn.size()); home_img = loadPixmap("../assets/images/button_home.png", home_btn.size());
flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size()); flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size());
settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio); settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio);
mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30));
connect(this, &Sidebar::valueChanged, [=] { update(); }); connect(this, &Sidebar::valueChanged, [=] { update(); });
@@ -48,15 +47,12 @@ void Sidebar::mousePressEvent(QMouseEvent *event) {
} else if (settings_btn.contains(event->pos())) { } else if (settings_btn.contains(event->pos())) {
settings_pressed = true; settings_pressed = true;
update(); update();
} else if (recording_audio && mic_indicator_btn.contains(event->pos())) {
mic_indicator_pressed = true;
update();
} }
} }
void Sidebar::mouseReleaseEvent(QMouseEvent *event) { void Sidebar::mouseReleaseEvent(QMouseEvent *event) {
if (flag_pressed || settings_pressed || mic_indicator_pressed) { if (flag_pressed || settings_pressed) {
flag_pressed = settings_pressed = mic_indicator_pressed = false; flag_pressed = settings_pressed = false;
update(); update();
} }
if (onroad && home_btn.contains(event->pos())) { if (onroad && home_btn.contains(event->pos())) {
@@ -65,8 +61,6 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) {
pm->send("userFlag", msg); pm->send("userFlag", msg);
} else if (settings_btn.contains(event->pos())) { } else if (settings_btn.contains(event->pos())) {
emit openSettings(); emit openSettings();
} else if (recording_audio && mic_indicator_btn.contains(event->pos())) {
emit openSettings(2, "RecordAudio");
} }
} }
@@ -112,8 +106,6 @@ void Sidebar::updateState(const UIState &s) {
pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color};
} }
setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); setProperty("pandaStatus", QVariant::fromValue(pandaStatus));
setProperty("recordingAudio", s.scene.recording_audio);
} }
void Sidebar::paintEvent(QPaintEvent *event) { void Sidebar::paintEvent(QPaintEvent *event) {
@@ -132,14 +124,6 @@ void Sidebar::drawSidebar(QPainter &p) {
p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img); p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img);
p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0); p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0);
p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img); p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img);
if (recording_audio) {
p.setBrush(danger_color);
p.setOpacity(mic_indicator_pressed ? 0.65 : 1.0);
p.drawRoundedRect(mic_indicator_btn, mic_indicator_btn.height() / 2, mic_indicator_btn.height() / 2);
int icon_x = mic_indicator_btn.x() + (mic_indicator_btn.width() - mic_img.width()) / 2;
int icon_y = mic_indicator_btn.y() + (mic_indicator_btn.height() - mic_img.height()) / 2;
p.drawPixmap(icon_x, icon_y, mic_img);
}
p.setOpacity(1.0); p.setOpacity(1.0);
// network // network
+2 -4
View File
@@ -23,7 +23,6 @@ class Sidebar : public QFrame {
Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged); Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged);
Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged); Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged);
Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged); Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged);
Q_PROPERTY(bool recordingAudio MEMBER recording_audio NOTIFY valueChanged);
public: public:
explicit Sidebar(QWidget* parent = 0); explicit Sidebar(QWidget* parent = 0);
@@ -43,8 +42,8 @@ protected:
void drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y); void drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y);
virtual void drawSidebar(QPainter &p); virtual void drawSidebar(QPainter &p);
QPixmap home_img, flag_img, settings_img, mic_img; QPixmap home_img, flag_img, settings_img;
bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed; bool onroad, flag_pressed, settings_pressed;
const QMap<cereal::DeviceState::NetworkType, QString> network_type = { const QMap<cereal::DeviceState::NetworkType, QString> network_type = {
{cereal::DeviceState::NetworkType::NONE, tr("--")}, {cereal::DeviceState::NetworkType::NONE, tr("--")},
{cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")}, {cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")},
@@ -57,7 +56,6 @@ protected:
const QRect home_btn = QRect(60, 860, 180, 180); const QRect home_btn = QRect(60, 860, 180, 180);
const QRect settings_btn = QRect(50, 35, 200, 117); const QRect settings_btn = QRect(50, 35, 200, 117);
const QRect mic_indicator_btn = QRect(158, 252, 75, 40);
const QColor good_color = QColor(255, 255, 255); const QColor good_color = QColor(255, 255, 255);
const QColor warning_color = QColor(218, 202, 37); const QColor warning_color = QColor(218, 202, 37);
const QColor danger_color = QColor(201, 34, 49); const QColor danger_color = QColor(201, 34, 49);
-139
View File
@@ -2,7 +2,6 @@
#include <QPushButton> #include <QPushButton>
#include <QButtonGroup> #include <QButtonGroup>
#include <QScroller>
#include "system/hardware/hw.h" #include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
@@ -335,141 +334,3 @@ QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStrin
} }
return ""; return "";
} }
TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items,
const QString &current, QWidget *parent) : DialogBase(parent) {
QFrame *container = new QFrame(this);
container->setStyleSheet(R"(
QFrame { background-color: #1B1B1B; }
#confirm_btn[enabled="false"] { background-color: #2B2B2B; }
#confirm_btn:enabled { background-color: #465BEA; }
#confirm_btn:enabled:pressed { background-color: #3049F4; }
QTreeWidget {
background-color: transparent;
border: none;
}
QTreeWidget::item {
height: 135;
padding: 0px 50px;
margin: 5px;
text-align: left;
font-size: 55px;
font-weight: 300;
border-radius: 10px;
background-color: #4F4F4F;
color: white;
}
QTreeWidget::item:selected {
background-color: #465BEA;
}
QTreeWidget::branch {
background-color: transparent;
}
)");
QVBoxLayout *main_layout = new QVBoxLayout(container);
main_layout->setContentsMargins(55, 50, 55, 50);
QLabel *title = new QLabel(prompt_text, this);
title->setStyleSheet("font-size: 70px; font-weight: 500;");
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
main_layout->addSpacing(25);
treeWidget = new QTreeWidget(this);
treeWidget->setHeaderHidden(true);
treeWidget->setIndentation(50);
treeWidget->setExpandsOnDoubleClick(false); // Disable double-click expansion
treeWidget->setAnimated(true);
treeWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
treeWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
treeWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
treeWidget->setDragEnabled(false);
treeWidget->setMouseTracking(true);
// Connect single-click to expand/collapse
QObject::connect(treeWidget, &QTreeWidget::itemClicked, [=](QTreeWidgetItem *item, int) {
if (item->childCount() > 0) {
item->setExpanded(!item->isExpanded());
treeWidget->scrollToItem(item->child(0), QAbstractItemView::EnsureVisible);
}
});
QScroller::grabGesture(treeWidget->viewport(), QScroller::LeftMouseButtonGesture);
// Populate tree
QListIterator<QPair<QString, QStringList>> iter(items);
while (iter.hasNext()) {
QPair currItem = iter.next();
if (currItem.first.isEmpty()) {
for (const QString &item : currItem.second) {
QTreeWidgetItem *topLevel = new QTreeWidgetItem();
topLevel->setText(0, item);
topLevel->setFlags(topLevel->flags() | Qt::ItemIsSelectable);
treeWidget->addTopLevelItem(topLevel);
if (item == current) {
topLevel->setSelected(true);
}
}
} else {
QTreeWidgetItem *folderItem = new QTreeWidgetItem(treeWidget);
folderItem->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png")));
folderItem->setText(0, " " + currItem.first);
folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate);
folderItem->setFlags(folderItem->flags() & ~Qt::ItemIsSelectable);
for (const QString &item : currItem.second)
{
QTreeWidgetItem *childItem = new QTreeWidgetItem(folderItem);
childItem->setText(0, item);
childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable);
if (item == current) {
childItem->setSelected(true);
folderItem->setExpanded(true);
}
}
}
}
confirm_btn = new QPushButton(tr("Select"));
confirm_btn->setObjectName("confirm_btn");
confirm_btn->setEnabled(false);
QObject::connect(treeWidget, &QTreeWidget::itemSelectionChanged, [=]() {
QList<QTreeWidgetItem*> selectedItems = treeWidget->selectedItems();
if (!selectedItems.isEmpty()) {
selection = selectedItems.first()->text(0);
confirm_btn->setEnabled(selection != current);
}
});
ScrollView *scroll_view = new ScrollView(treeWidget, this);
scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
main_layout->addWidget(scroll_view);
main_layout->addSpacing(35);
// cancel + confirm buttons
QHBoxLayout *blayout = new QHBoxLayout;
main_layout->addLayout(blayout);
blayout->setSpacing(50);
QPushButton *cancel_btn = new QPushButton(tr("Cancel"));
QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject);
QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept);
blayout->addWidget(cancel_btn);
blayout->addWidget(confirm_btn);
QVBoxLayout *outer_layout = new QVBoxLayout(this);
outer_layout->setContentsMargins(50, 50, 50, 50);
outer_layout->addWidget(container);
}
QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items,
const QString &current, QWidget *parent) {
TreeOptionDialog d(prompt_text, items, current, parent);
if (d.exec()) {
return d.selection;
}
return "";
}
-14
View File
@@ -6,7 +6,6 @@
#include <QString> #include <QString>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <QTreeWidget>
#include "selfdrive/ui/qt/widgets/keyboard.h" #include "selfdrive/ui/qt/widgets/keyboard.h"
@@ -70,16 +69,3 @@ public:
static QString getSelection(const QString &prompt_text, const QStringList &l, const QString &current, QWidget *parent); static QString getSelection(const QString &prompt_text, const QStringList &l, const QString &current, QWidget *parent);
QString selection; QString selection;
}; };
class TreeOptionDialog : public DialogBase {
Q_OBJECT
public:
explicit TreeOptionDialog(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items, const QString &current, QWidget *parent = nullptr);
static QString getSelection(const QString &prompt_text, const QList<QPair<QString, QStringList>> &items, const QString &current, QWidget *parent = nullptr);
QString selection;
private:
QTreeWidget *treeWidget;
QPushButton *confirm_btn;
};
+3 -3
View File
@@ -139,7 +139,7 @@ class Soundd(QuietMode):
# sounddevice must be imported after forking processes # sounddevice must be imported after forking processes
import sounddevice as sd import sounddevice as sd
sm = messaging.SubMaster(['selfdriveState', 'soundPressure']) sm = messaging.SubMaster(['selfdriveState', 'microphone'])
with self.get_stream(sd) as stream: with self.get_stream(sd) as stream:
rk = Ratekeeper(20) rk = Ratekeeper(20)
@@ -150,8 +150,8 @@ class Soundd(QuietMode):
self.load_param() self.load_param()
if sm.updated['soundPressure'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb) self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb)
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
self.get_audible_alert(sm) self.get_audible_alert(sm)
-1
View File
@@ -21,7 +21,6 @@ qt_src = [
"sunnypilot/qt/home.cc", "sunnypilot/qt/home.cc",
"sunnypilot/qt/offroad/exit_offroad_button.cc", "sunnypilot/qt/offroad/exit_offroad_button.cc",
"sunnypilot/qt/offroad/offroad_home.cc", "sunnypilot/qt/offroad/offroad_home.cc",
"sunnypilot/qt/offroad/settings/developer_panel.cc",
"sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/device_panel.cc",
"sunnypilot/qt/offroad/settings/lateral_panel.cc", "sunnypilot/qt/offroad/settings/lateral_panel.cc",
"sunnypilot/qt/offroad/settings/longitudinal_panel.cc", "sunnypilot/qt/offroad/settings/longitudinal_panel.cc",
@@ -1,80 +0,0 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h"
DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(parent) {
// Advanced Controls Toggle
showAdvancedControls = new ParamControlSP("ShowAdvancedControls", tr("Show Advanced Controls"), tr("Toggle visibility of advanced sunnypilot controls.\nThis only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state."), "");
addItem(showAdvancedControls);
QObject::connect(showAdvancedControls, &ParamControlSP::toggleFlipped, this, [=](bool) {
AbstractControlSP::UpdateAllAdvancedControls();
updateToggles(!uiState()->scene.started);
});
showAdvancedControls->showDescription();
// Github Runner Toggle
enableGithubRunner = new ParamControlSP("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), "", this, true);
addItem(enableGithubRunner);
// Quickboot Mode Toggle
prebuiltToggle = new ParamControlSP("QuickBootToggle", tr("Enable Quickboot Mode"), tr(""), "", this, true);
addItem(prebuiltToggle);
QObject::connect(prebuiltToggle, &ParamControl::toggleFlipped, [=](bool state) {
QString prebuiltPath = "/data/openpilot/prebuilt";
state ? QFile(prebuiltPath).open(QIODevice::WriteOnly) : QFile::remove(prebuiltPath);
prebuiltToggle->refresh();
});
prebuiltToggle->setVisible(false);
// Error log button
errorLogBtn = new ButtonControlSP(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."));
connect(errorLogBtn, &ButtonControlSP::clicked, [=]() {
QFileInfo file("/data/community/crashes/error.log");
QString text;
if (file.exists()) {
text = "<b>" + file.lastModified().toString("dd-MMM-yyyy hh:mm:ss ").toUpper() + "</b><br><br>";
}
text += QString::fromStdString(util::read_file("/data/community/crashes/error.log"));
ConfirmationDialog::rich(text, this);
});
addItem(errorLogBtn);
QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanelSP::updateToggles);
}
void DeveloperPanelSP::updateToggles(bool offroad) {
bool is_release = params.getBool("IsReleaseBranch");
bool is_tested = params.getBool("IsTestedBranch");
bool is_development = params.getBool("IsDevelopmentBranch");
bool disable_updates = params.getBool("DisableUpdates");
prebuiltToggle->setVisible(!is_release && !is_tested && !is_development);
prebuiltToggle->setEnabled(disable_updates);
params.putBool("QuickBootToggle", QFile::exists("/data/openpilot/prebuilt"));
prebuiltToggle->refresh();
prebuiltToggle->setDescription(disable_updates
? tr("When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, "
"it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. "
"<br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b>")
: tr("Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first."));
enableGithubRunner->setVisible(!is_release);
errorLogBtn->setVisible(!is_release);
showAdvancedControls->setEnabled(true);
}
void DeveloperPanelSP::showEvent(QShowEvent *event) {
DeveloperPanel::showEvent(event);
updateToggles(!uiState()->scene.started);
AbstractControlSP::UpdateAllAdvancedControls();
prebuiltToggle->showDescription();
}
@@ -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.
*/
#pragma once
#include <QFile>
#include <QFileInfo>
#include "selfdrive/ui/qt/offroad/developer_panel.h"
class DeveloperPanelSP : public DeveloperPanel {
Q_OBJECT
public:
explicit DeveloperPanelSP(SettingsWindow *parent);
private:
ParamControlSP *enableGithubRunner;
ButtonControlSP *errorLogBtn;
ParamControlSP *prebuiltToggle;
Params params;
ParamControlSP *showAdvancedControls;
private slots:
void updateToggles(bool offroad);
protected:
void showEvent(QShowEvent *event) override;
};
@@ -87,17 +87,6 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
params.put("DeviceBootMode", QString::number(index).toStdString()); params.put("DeviceBootMode", QString::number(index).toStdString());
updateState(); updateState();
}); });
interactivityTimeout = new OptionControlSP("InteractivityTimeout", tr("Interactivity Timeout"),
tr("Apply a custom timeout for settings UI."
"\nThis is the time after which settings UI closes automatically if user is not interacting with the screen."),
"", {0, 120}, 10, true, nullptr, false);
connect(interactivityTimeout, &OptionControlSP::updateLabels, [=]() {
updateState();
});
addItem(interactivityTimeout);
// Brightness // Brightness
brightness = new Brightness(); brightness = new Brightness();
@@ -209,11 +198,4 @@ void DevicePanelSP::updateState() {
currStatus = DeviceSleepModeStatus::OFFROAD; currStatus = DeviceSleepModeStatus::OFFROAD;
} }
toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus)); toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus));
QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout"));
if (timeoutValue == "0") {
interactivityTimeout->setLabel("Default");
} else {
interactivityTimeout->setLabel(timeoutValue + "s");
}
} }
@@ -33,7 +33,6 @@ private:
MaxTimeOffroad *maxTimeOffroad; MaxTimeOffroad *maxTimeOffroad;
ButtonParamControlSP *toggleDeviceBootMode; ButtonParamControlSP *toggleDeviceBootMode;
Brightness *brightness; Brightness *brightness;
OptionControlSP *interactivityTimeout;
const QString alwaysOffroadStyle = R"( const QString alwaysOffroadStyle = R"(
PushButtonSP { PushButtonSP {
@@ -8,8 +8,6 @@
#include <algorithm> #include <algorithm>
#include <QJsonDocument> #include <QJsonDocument>
#include <QStyle> #include <QStyle>
#include <QtConcurrent/QtConcurrent>
#include <QDir>
#include "common/model.h" #include "common/model.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
@@ -68,11 +66,6 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels); connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels);
list->addItem(currentModelLblBtn); list->addItem(currentModelLblBtn);
clearModelCacheBtn = new ButtonControlSP(tr("Clear Model Cache"), tr("CLEAR"), "", this);
connect(clearModelCacheBtn, &ButtonControlSP::clicked, this, &ModelsPanel::clearModelCache);
list->addItem(clearModelCacheBtn);
// Create progress bars for downloads // Create progress bars for downloads
supercomboProgressBar = createProgressBar(this); supercomboProgressBar = createProgressBar(this);
QString supercomboType = tr("Driving Model"); QString supercomboType = tr("Driving Model");
@@ -97,25 +90,11 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
list->addItem(horizontal_line()); list->addItem(horizontal_line());
// LiveDelay toggle // LiveDelay toggle
lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png"); list->addItem(new ParamControlSP("LagdToggle",
lagd_toggle_control->showDescription(); tr("Live Learning Steer Delay"),
list->addItem(lagd_toggle_control); 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."),
// Software delay control "../assets/offroad/icon_shell.png"));
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, 30}, 1, false, nullptr, true, true);
connect(delay_control, &OptionControlSP::updateLabels, [=]() {
float value = QString::fromStdString(params.get("LagdToggledelay")).toFloat();
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
});
connect(lagd_toggle_control, &ParamControlSP::toggleFlipped, [=](bool state) {
delay_control->setVisible(!state);
});
delay_control->showDescription();
list->addItem(delay_control);
} }
QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) { QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) {
@@ -251,73 +230,28 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
currentModelLblBtn->setEnabled(false); currentModelLblBtn->setEnabled(false);
currentModelLblBtn->setValue(tr("Fetching models...")); currentModelLblBtn->setValue(tr("Fetching models..."));
struct ModelEntry { // Create mapping of bundle indices to display names
QString folder; QMap<uint32_t, QString> index_to_bundle;
QString displayName;
int index;
};
QList<ModelEntry> sortedModels;
QSet<QString> modelFolders;
const auto bundles = model_manager.getAvailableBundles(); const auto bundles = model_manager.getAvailableBundles();
for (const auto &bundle: bundles) {
for (const auto &bundle : bundles) { index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName()));
auto overrides = bundle.getOverrides();
QString gen;
for (const auto &override : overrides) {
if (override.getKey() == "folder") {
gen = QString::fromStdString(override.getValue().cStr());
}
}
modelFolders.insert(gen);
sortedModels.append(ModelEntry{
gen,
QString::fromStdString(bundle.getDisplayName()),
static_cast<int>(bundle.getIndex())
});
} }
std::sort(sortedModels.begin(), sortedModels.end(), // Sort bundles by index in descending order
[](const ModelEntry &a, const ModelEntry &b) { QStringList bundleNames;
return a.index > b.index; // Add "Default" as the first option
}); bundleNames.append(DEFAULT_MODEL);
// Create a list of folder-maxIndex pairs for sorting auto indices = index_to_bundle.keys();
QList<QPair<QString, int>> folderMaxIndices; std::sort(indices.begin(), indices.end(), std::greater<uint32_t>());
for (const auto &folder : modelFolders) { for (const auto &index: indices) {
int maxIndex = -1; bundleNames.append(index_to_bundle[index]);
for (const auto &model : sortedModels) {
if (model.folder == folder) {
maxIndex = std::max(maxIndex, model.index);
}
}
folderMaxIndices.append(qMakePair(folder, maxIndex));
} }
// Sort folders by their highest model index
std::sort(folderMaxIndices.begin(), folderMaxIndices.end(),
[](const QPair<QString, int> &a, const QPair<QString, int> &b) {
return a.second > b.second;
});
// Create the final items list using sorted folders
QList<QPair<QString, QStringList>> items;
for (const auto &folderPair : folderMaxIndices) {
QStringList folderModels;
for (const auto &model : sortedModels) {
if (model.folder == folderPair.first) {
folderModels.append(model.displayName);
}
}
items.append(qMakePair(folderPair.first, folderModels));
}
items.insert(0, qMakePair(QString(""), QStringList{DEFAULT_MODEL}));
currentModelLblBtn->setValue(GetActiveModelInternalName()); currentModelLblBtn->setValue(GetActiveModelInternalName());
const QString selectedBundleName = TreeOptionDialog::getSelection( const QString selectedBundleName = MultiOptionDialog::getSelection(
tr("Select a Model"), items, GetActiveModelName(), this); tr("Select a Model"), bundleNames, GetActiveModelName(), this);
if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) {
return; return;
@@ -356,26 +290,6 @@ void ModelsPanel::updateLabels() {
handleBundleDownloadProgress(); handleBundleDownloadProgress();
currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); currentModelLblBtn->setEnabled(!is_onroad && !isDownloading());
currentModelLblBtn->setValue(GetActiveModelInternalName()); currentModelLblBtn->setValue(GetActiveModelInternalName());
// Update lagdToggle description with current value
QString desc = tr("Enable this for the car to learn and adapt its steering response time. "
"Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. "
"The Current value is updated automatically when the vehicle is Onroad.");
QString current = QString::fromStdString(params.get("LagdToggleDesc", false));
if (!current.isEmpty()) {
desc += "<br><br><b><span style=\"color:#e0e0e0\">" + tr("Current:") + "</span></b> <span style=\"color:#e0e0e0\">" + current + "</span>";
}
lagd_toggle_control->setDescription(desc);
lagd_toggle_control->showDescription();
delay_control->setVisible(!params.getBool("LagdToggle"));
if (delay_control->isVisible()) {
float value = QString::fromStdString(params.get("LagdToggledelay")).toFloat();
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
delay_control->showDescription();
}
clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB");
} }
/** /**
@@ -396,32 +310,3 @@ void ModelsPanel::showResetParamsDialog() {
params.remove("LiveTorqueParameters"); params.remove("LiveTorqueParameters");
} }
} }
void ModelsPanel::clearModelCache() {
QString confirmMsg = tr("This will delete ALL downloaded models from the cache"
"<br/><u>except the currently active model</u>."
"<br/><br/>Are you sure you want to continue?");
QString content("<body><h2 style=\"text-align: center;\">" + tr("Driving Model Selector") + "</h2><br>"
"<p style=\"text-align: center; margin: 0 128px; font-size: 50px;\">" + confirmMsg + "</p></body>");
if (showConfirmationDialog(
content,
tr("Clear Cache"))) {
params.putBool("ModelManager_ClearCache", true);
}
}
double ModelsPanel::calculateCacheSize() {
QFuture<qint64> future_ModelCacheSize = QtConcurrent::run([=]() {
QDir model_dir(QString::fromStdString(Path::model_root()));
QFileInfoList model_files = model_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
qint64 totalSize = 0;
for (const QFileInfo &model_file : model_files) {
if (model_file.isFile()) {
totalSize += model_file.size();
}
}
return totalSize;
});
return static_cast<double>(future_ModelCacheSize) / (1024.0 * 1024.0);
}
@@ -41,8 +41,6 @@ private:
cereal::ModelManagerSP::Reader model_manager; cereal::ModelManagerSP::Reader model_manager;
cereal::ModelManagerSP::DownloadStatus download_status{}; cereal::ModelManagerSP::DownloadStatus download_status{};
cereal::ModelManagerSP::DownloadStatus prev_download_status{}; cereal::ModelManagerSP::DownloadStatus prev_download_status{};
void clearModelCache();
double calculateCacheSize();
bool canContinueOnMeteredDialog() { bool canContinueOnMeteredDialog() {
if (!is_metered) return true; if (!is_metered) return true;
@@ -66,8 +64,6 @@ private:
bool is_onroad = false; bool is_onroad = false;
ButtonControlSP *currentModelLblBtn; ButtonControlSP *currentModelLblBtn;
ParamControlSP *lagd_toggle_control;
OptionControlSP *delay_control;
QProgressBar *supercomboProgressBar; QProgressBar *supercomboProgressBar;
QFrame *supercomboFrame; QFrame *supercomboFrame;
QProgressBar *navigationProgressBar; QProgressBar *navigationProgressBar;
@@ -77,6 +73,5 @@ private:
QProgressBar *policyProgressBar; QProgressBar *policyProgressBar;
QFrame *policyFrame; QFrame *policyFrame;
Params params; Params params;
ButtonControlSP *clearModelCacheBtn;
}; };
@@ -8,10 +8,10 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
#include "selfdrive/ui/qt/offroad/developer_panel.h"
#include "selfdrive/ui/qt/offroad/firehose.h" #include "selfdrive/ui/qt/offroad/firehose.h"
#include "selfdrive/ui/sunnypilot/qt/network/networking.h" #include "selfdrive/ui/sunnypilot/qt/network/networking.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h"
@@ -91,7 +91,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"),
PanelInfo(" " + tr("Developer"), new DeveloperPanelSP(this), "../assets/icons/shell.png"), PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/icons/shell.png"),
}; };
nav_btns = new QButtonGroup(this); nav_btns = new QButtonGroup(this);
@@ -18,18 +18,6 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) {
searchBranches(d.text()); searchBranches(d.text());
} }
}); });
// Disable Updates toggle
disableUpdatesToggle = new ParamControl("DisableUpdates",
tr("Disable Updates"),
tr("When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b>"),
"../assets/icons/icon_warning.png",
this, true);
disableUpdatesToggle->showDescription();
addItem(disableUpdatesToggle);
connect(disableUpdatesToggle, &ParamControl::toggleFlipped, this, &SoftwarePanelSP::handleDisableUpdatesToggled);
connect(uiState(), &UIState::offroadTransition, this, &SoftwarePanelSP::updateDisableUpdatesToggle);
updateDisableUpdatesToggle(!uiState()->scene.started);
} }
/** /**
@@ -61,27 +49,3 @@ void SoftwarePanelSP::searchBranches(const QString &query) {
checkForUpdates(); checkForUpdates();
} }
} }
void SoftwarePanelSP::handleDisableUpdatesToggled(bool state) {
if (ConfirmationDialog::confirm(tr("%1 updates requires a reboot.<br>Reboot now?")
.arg(state ? "Disabling" : "Enabling"), tr("Reboot"), this)) {
params.putBool("DoReboot", true);
} else {
params.putBool("DisableUpdates", !state);
disableUpdatesToggle->refresh();
}
}
void SoftwarePanelSP::updateDisableUpdatesToggle(bool offroad) {
bool enabled = offroad;
disableUpdatesToggle->setEnabled(enabled);
disableUpdatesToggle->setDescription(enabled
? tr("When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b>")
: tr("Please enable always offroad mode or turn off vehicle to adjust these toggles"));
}
void SoftwarePanelSP::showEvent(QShowEvent *event) {
SoftwarePanel::showEvent(event);
updateDisableUpdatesToggle(!uiState()->scene.started);
disableUpdatesToggle->showDescription();
}
@@ -19,10 +19,4 @@ public:
private: private:
void searchBranches(const QString &query); void searchBranches(const QString &query);
ParamControl *disableUpdatesToggle = nullptr;
void handleDisableUpdatesToggled(bool state);
private slots:
void updateDisableUpdatesToggle(bool offroad);
protected:
void showEvent(QShowEvent *event) override;
}; };
@@ -12,7 +12,7 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString &param_name, const QString &param_value) { connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString &param_name, const QString &param_value) {
paramsRefresh(); paramsRefresh();
}); });
main_layout = new QStackedLayout(this); main_layout = new QStackedLayout(this);
ListWidgetSP *list = new ListWidgetSP(this, false); ListWidgetSP *list = new ListWidgetSP(this, false);
@@ -30,7 +30,6 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
}, },
}; };
// Add regular toggles first
for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) {
auto toggle = new ParamControlSP(param, title, desc, icon, this); auto toggle = new ParamControlSP(param, title, desc, icon, this);
@@ -54,20 +53,9 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
param_watcher->addParam(param); param_watcher->addParam(param);
} }
// Visuals: Display Metrics below Chevron
std::vector<QString> chevron_info_settings_texts{tr("Off"), tr("Distance"), tr("Speed"), tr("Time"), tr("All")};
chevron_info_settings = new ButtonParamControlSP(
"ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control)."),
"",
chevron_info_settings_texts,
200);
chevron_info_settings->showDescription();
list->addItem(chevron_info_settings);
param_watcher->addParam("ChevronInfo");
sunnypilotScroller = new ScrollViewSP(list, this); sunnypilotScroller = new ScrollViewSP(list, this);
vlayout->addWidget(sunnypilotScroller); vlayout->addWidget(sunnypilotScroller);
main_layout->addWidget(sunnypilotScreen); main_layout->addWidget(sunnypilotScreen);
} }
@@ -79,8 +67,4 @@ void VisualsPanel::paramsRefresh() {
for (auto toggle : toggles) { for (auto toggle : toggles) {
toggle.second->refresh(); toggle.second->refresh();
} }
if (chevron_info_settings) {
chevron_info_settings->refresh();
}
} }
@@ -27,5 +27,4 @@ protected:
Params params; Params params;
std::map<std::string, ParamControlSP*> toggles; std::map<std::string, ParamControlSP*> toggles;
ParamWatcher * param_watcher; ParamWatcher * param_watcher;
ButtonParamControlSP *chevron_info_settings;
}; };
+8 -23
View File
@@ -30,24 +30,9 @@ QFrame *vertical_space(int height, QWidget *parent) {
} }
// AbstractControlSP // AbstractControlSP
std::vector<AbstractControlSP*> AbstractControlSP::advanced_controls_;
AbstractControlSP::~AbstractControlSP() { UnregisterAdvancedControl(this); }
void AbstractControlSP::RegisterAdvancedControl(AbstractControlSP *ctrl) { advanced_controls_.push_back(ctrl); } AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent)
: AbstractControl(title, desc, icon, parent) {
void AbstractControlSP::UnregisterAdvancedControl(AbstractControlSP *ctrl) {
advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), ctrl), advanced_controls_.end());
}
void AbstractControlSP::UpdateAllAdvancedControls() {
bool visibility = Params().getBool("ShowAdvancedControls");
advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), nullptr), advanced_controls_.end());
for (auto *ctrl : advanced_controls_) ctrl->setVisible(visibility);
}
AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl)
: AbstractControl(title, desc, icon, parent), isAdvancedControl(advancedControl) {
if (isAdvancedControl) RegisterAdvancedControl(this);
main_layout = new QVBoxLayout(this); main_layout = new QVBoxLayout(this);
main_layout->setMargin(0); main_layout->setMargin(0);
@@ -97,8 +82,8 @@ void AbstractControlSP::hideEvent(QHideEvent *e) {
} }
} }
AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent)
: AbstractControlSP(title, desc, icon, parent, advancedControl) { : AbstractControlSP(title, desc, icon, parent) {
if (title_label != nullptr) { if (title_label != nullptr) {
delete title_label; delete title_label;
@@ -184,8 +169,8 @@ void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) {
// controls // controls
ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent, bool advancedControl) ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent)
: AbstractControlSP(title, desc, "", parent, advancedControl) { : AbstractControlSP(title, desc, "", parent) {
btn.setText(text); btn.setText(text);
btn.setStyleSheet(R"( btn.setStyleSheet(R"(
@@ -240,8 +225,8 @@ void ElidedLabelSP::paintEvent(QPaintEvent *event) {
// ParamControlSP // ParamControlSP
ParamControlSP::ParamControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) ParamControlSP::ParamControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon, QWidget *parent)
: ToggleControlSP(title, desc, icon, false, parent, advancedControl){ : ToggleControlSP(title, desc, icon, false, parent) {
key = param.toStdString(); key = param.toStdString();
QObject::connect(this, &ParamControlSP::toggleFlipped, this, &ParamControlSP::toggleClicked); QObject::connect(this, &ParamControlSP::toggleFlipped, this, &ParamControlSP::toggleClicked);
+12 -47
View File
@@ -57,7 +57,6 @@ class AbstractControlSP : public AbstractControl {
Q_OBJECT Q_OBJECT
public: public:
~AbstractControlSP();
void setDescription(const QString &desc) override { void setDescription(const QString &desc) override {
if (description) description->setText(desc); if (description) description->setText(desc);
} }
@@ -82,30 +81,13 @@ public slots:
description->setVisible(true); description->setVisible(true);
} }
void setVisible(bool visible) override {
bool _visible = visible;
if (isAdvancedControl && !params.getBool("ShowAdvancedControls")) {
_visible = false;
}
AbstractControl::setVisible(_visible);
}
static void RegisterAdvancedControl(AbstractControlSP *ctrl);
static void UnregisterAdvancedControl(AbstractControlSP *ctrl);
static void UpdateAllAdvancedControls();
protected: protected:
AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr, bool advancedControl = false); AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr);
void hideEvent(QHideEvent *e) override; void hideEvent(QHideEvent *e) override;
QVBoxLayout *main_layout; QVBoxLayout *main_layout;
ElidedLabelSP *value; ElidedLabelSP *value;
QLabel *description = nullptr; QLabel *description = nullptr;
bool isAdvancedControl;
private:
Params params;
static std::vector<AbstractControlSP*> advanced_controls_;
}; };
// AbstractControlSP_SELECTOR // AbstractControlSP_SELECTOR
@@ -115,7 +97,7 @@ class AbstractControlSP_SELECTOR : public AbstractControlSP {
protected: protected:
QSpacerItem *spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); QSpacerItem *spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed);
AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr, bool advancedControl = false); AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr);
void hideEvent(QHideEvent *e) override; void hideEvent(QHideEvent *e) override;
}; };
@@ -141,7 +123,7 @@ class ButtonControlSP : public AbstractControlSP {
Q_OBJECT Q_OBJECT
public: public:
ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr, bool advancedControl = false); ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr);
inline void setText(const QString &text) { btn.setText(text); } inline void setText(const QString &text) { btn.setText(text); }
inline QString text() const { return btn.text(); } inline QString text() const { return btn.text(); }
inline void click() { btn.click(); } inline void click() { btn.click(); }
@@ -160,7 +142,7 @@ class ToggleControlSP : public AbstractControlSP {
Q_OBJECT Q_OBJECT
public: public:
ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr, bool advancedControl = false) : AbstractControlSP(title, desc, icon, parent, advancedControl) { ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControlSP(title, desc, icon, parent) {
// space between toggle and title // space between toggle and title
icon_label = new QLabel(this); icon_label = new QLabel(this);
hlayout->addWidget(icon_label); hlayout->addWidget(icon_label);
@@ -191,7 +173,7 @@ class ParamControlSP : public ToggleControlSP {
Q_OBJECT Q_OBJECT
public: public:
ParamControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr, bool advancedControl = false); ParamControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr);
void setConfirmation(bool _confirm, bool _store_confirm) { void setConfirmation(bool _confirm, bool _store_confirm) {
confirm = _confirm; confirm = _confirm;
store_confirm = _store_confirm; store_confirm = _store_confirm;
@@ -237,7 +219,7 @@ class MultiButtonControlSP : public AbstractControlSP_SELECTOR {
public: public:
MultiButtonControlSP(const QString &title, const QString &desc, const QString &icon, MultiButtonControlSP(const QString &title, const QString &desc, const QString &icon,
const std::vector<QString> &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), button_texts(button_texts), is_inline_layout(inline_layout) { const std::vector<QString> &button_texts, const int minimum_button_width = 225, const bool inline_layout = false) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts), is_inline_layout(inline_layout) {
const QString style = R"( const QString style = R"(
QPushButton { QPushButton {
border-radius: 20px; border-radius: 20px;
@@ -389,8 +371,8 @@ class ButtonParamControlSP : public MultiButtonControlSP {
Q_OBJECT Q_OBJECT
public: public:
ButtonParamControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon, ButtonParamControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon,
const std::vector<QString> &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon, const std::vector<QString> &button_texts, const int minimum_button_width = 225, const bool inline_layout = false) : MultiButtonControlSP(title, desc, icon,
button_texts, minimum_button_width, inline_layout, advancedControl) { button_texts, minimum_button_width, inline_layout) {
key = param.toStdString(); key = param.toStdString();
int value = atoi(params.get(key).c_str()); int value = atoi(params.get(key).c_str());
@@ -498,16 +480,6 @@ private:
return result.toInt(); return result.toInt();
} }
int getParamValueScaled() {
const auto param_value = QString::fromStdString(params.get(key));
return static_cast<int>(param_value.toFloat() * 100);
}
void setParamValueScaled(const int new_value) {
const float scaled_value = new_value / 100.0f;
params.put(key, QString::number(scaled_value, 'f', 2).toStdString());
}
// Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value. // Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value.
void setParamValue(const int new_value) { void setParamValue(const int new_value) {
const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value); const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value);
@@ -516,8 +488,7 @@ private:
public: public:
OptionControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon, OptionControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon,
const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap<QString, QString> *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), is_inline_layout(inline_layout) {
const QMap<QString, QString> *valMap = nullptr, bool scale_float = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), _title(title), valueMap(valMap), is_inline_layout(inline_layout), use_float_scaling(scale_float) {
const QString style = R"( const QString style = R"(
QPushButton { QPushButton {
border-radius: 20px; border-radius: 20px;
@@ -557,7 +528,7 @@ public:
const std::vector<QString> button_texts{"", ""}; const std::vector<QString> button_texts{"", ""};
key = param.toStdString(); key = param.toStdString();
value = use_float_scaling ? getParamValueScaled() : getParamValue(); value = getParamValue();
button_group = new QButtonGroup(this); button_group = new QButtonGroup(this);
button_group->setExclusive(true); button_group->setExclusive(true);
@@ -575,15 +546,10 @@ public:
QObject::connect(button, &QPushButton::clicked, [=]() { QObject::connect(button, &QPushButton::clicked, [=]() {
int change_value = (i == 0) ? -per_value_change : per_value_change; int change_value = (i == 0) ? -per_value_change : per_value_change;
value = use_float_scaling ? getParamValueScaled() : getParamValue(); value = getParamValue(); // in case it changed externally, we need to get the latest value.
value += change_value; value += change_value;
value = std::clamp(value, range.min_value, range.max_value); value = std::clamp(value, range.min_value, range.max_value);
setParamValue(value);
if (use_float_scaling) {
setParamValueScaled(value);
} else {
setParamValue(value);
}
button_group->button(0)->setEnabled(!(value <= range.min_value)); button_group->button(0)->setEnabled(!(value <= range.min_value));
button_group->button(1)->setEnabled(!(value >= range.max_value)); button_group->button(1)->setEnabled(!(value >= range.max_value));
@@ -676,7 +642,6 @@ private:
const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;";
bool button_enabled = true; bool button_enabled = true;
bool use_float_scaling = false;
}; };
class PushButtonSP : public QPushButton { class PushButtonSP : public QPushButton {
+1 -5
View File
@@ -61,9 +61,6 @@ void update_state(UIState *s) {
scene.light_sensor = -1; scene.light_sensor = -1;
} }
scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition; scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition;
auto params = Params();
scene.recording_audio = params.getBool("RecordAudio") && scene.started;
} }
void ui_update_params(UIState *s) { void ui_update_params(UIState *s) {
@@ -167,9 +164,8 @@ void Device::setAwake(bool on) {
} }
void Device::resetInteractiveTimeout(int timeout) { void Device::resetInteractiveTimeout(int timeout) {
int customTimeout = QString::fromStdString(Params().get("InteractivityTimeout")).toInt();
if (timeout == -1) { if (timeout == -1) {
timeout = customTimeout == 0 ? (ignition_on ? 10 : 30) : customTimeout; timeout = (ignition_on ? 10 : 30);
} }
interactive_timeout = timeout * UI_FREQ; interactive_timeout = timeout * UI_FREQ;
} }
+1 -1
View File
@@ -62,7 +62,7 @@ typedef struct UIScene {
cereal::LongitudinalPersonality personality; cereal::LongitudinalPersonality personality;
float light_sensor = -1; float light_sensor = -1;
bool started, ignition, is_metric, recording_audio; bool started, ignition, is_metric;
uint64_t started_frame; uint64_t started_frame;
} UIScene; } UIScene;
+5 -64
View File
@@ -1,20 +1,12 @@
import pyray as rl import pyray as rl
import numpy as np
import time import time
import threading
from collections.abc import Callable from collections.abc import Callable
from enum import Enum from enum import Enum
from cereal import messaging, log from cereal import messaging, log
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params, UnknownKeyName from openpilot.common.params import Params, UnknownKeyName
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import DEFAULT_FPS
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app
UI_BORDER_SIZE = 30 UI_BORDER_SIZE = 30
BACKLIGHT_OFFROAD = 50
class UIStatus(Enum): class UIStatus(Enum):
@@ -147,15 +139,10 @@ class UIState:
class Device: class Device:
def __init__(self): def __init__(self):
self._ignition = False self._ignition = False
self._interaction_time: float = -1 self._interaction_time: float = 0.0
self._interactive_timeout_callbacks: list[Callable] = [] self._interactive_timeout_callbacks: list[Callable] = []
self._prev_timed_out = False self._prev_timed_out = False
self._awake = False self.reset_interactive_timeout()
self._offroad_brightness: int = BACKLIGHT_OFFROAD
self._last_brightness: int = 0
self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / DEFAULT_FPS)
self._brightness_thread: threading.Thread | None = None
def reset_interactive_timeout(self, timeout: int = -1) -> None: def reset_interactive_timeout(self, timeout: int = -1) -> None:
if timeout == -1: if timeout == -1:
@@ -166,64 +153,18 @@ class Device:
self._interactive_timeout_callbacks.append(callback) self._interactive_timeout_callbacks.append(callback)
def update(self): def update(self):
# do initial reset
if self._interaction_time <= 0:
self.reset_interactive_timeout()
self._update_brightness()
self._update_wakefulness()
def set_offroad_brightness(self, brightness: int):
# TODO: not yet used, should be used in prime widget for QR code, etc.
self._offroad_brightness = min(max(brightness, 0), 100)
def _update_brightness(self):
clipped_brightness = self._offroad_brightness
if ui_state.started and ui_state.light_sensor >= 0:
clipped_brightness = ui_state.light_sensor
# CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm
if clipped_brightness <= 8:
clipped_brightness = clipped_brightness / 903.3
else:
clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0
clipped_brightness = float(np.clip(100 * clipped_brightness, 10, 100))
brightness = round(self._brightness_filter.update(clipped_brightness))
if not self._awake:
brightness = 0
if brightness != self._last_brightness:
if self._brightness_thread is None or not self._brightness_thread.is_alive():
cloudlog.debug(f"setting display brightness {brightness}")
self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,))
self._brightness_thread.start()
self._last_brightness = brightness
def _update_wakefulness(self):
# Handle interactive timeout # Handle interactive timeout
ignition_just_turned_off = not ui_state.ignition and self._ignition ignition_just_turned_off = not ui_state.ignition and self._ignition
self._ignition = ui_state.ignition self._ignition = ui_state.ignition
if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events):
self.reset_interactive_timeout()
interaction_timeout = time.monotonic() > self._interaction_time interaction_timeout = time.monotonic() > self._interaction_time
if interaction_timeout and not self._prev_timed_out: if ignition_just_turned_off or rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
self.reset_interactive_timeout()
elif interaction_timeout and not self._prev_timed_out:
for callback in self._interactive_timeout_callbacks: for callback in self._interactive_timeout_callbacks:
callback() callback()
self._prev_timed_out = interaction_timeout self._prev_timed_out = interaction_timeout
self._set_awake(ui_state.ignition or not interaction_timeout)
def _set_awake(self, on: bool):
if on != self._awake:
self._awake = on
cloudlog.debug(f"setting display power {int(on)}")
HARDWARE.set_display_power(on)
# Global instance # Global instance
ui_state = UIState() ui_state = UIState()
View File
-51
View File
@@ -1,51 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
class LagdToggle:
def __init__(self):
self.params = Params()
self.lag = 0.0
self._last_desc = None
@property
def software_delay(self):
return float(self.params.get("LagdToggledelay", encoding='utf8'))
def _maybe_update_desc(self, desc):
if desc != self._last_desc:
self.params.put_nonblocking("LagdToggleDesc", desc)
self._last_desc = desc
def lagd_main(self, CP, sm, model):
if self.params.get_bool("LagdToggle"):
lateral_delay = sm["liveDelay"].lateralDelay
lat_smooth = model.LAT_SMOOTH_SECONDS
result = lateral_delay + lat_smooth
desc = f"live steer delay learner ({lateral_delay:.3f}s) + model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)"
self._maybe_update_desc(desc)
return result
steer_actuator_delay = CP.steerActuatorDelay
lat_smooth = model.LAT_SMOOTH_SECONDS
delay = self.software_delay
result = (steer_actuator_delay + delay) + lat_smooth
desc = (f"Vehicle steering delay ({steer_actuator_delay:.3f}s) + software delay ({delay:.3f}s) + " +
f"model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)")
self._maybe_update_desc(desc)
return result
def lagd_torqued_main(self, CP, msg):
if self.params.get_bool("LagdToggle"):
self.lag = msg.lateralDelay
cloudlog.debug(f"TORQUED USING LIVE DELAY: {self.lag:.3f}")
else:
self.lag = CP.steerActuatorDelay + self.software_delay
cloudlog.debug(f"TORQUED USING STEER ACTUATOR: {self.lag:.3f}")
return self.lag
+1 -1
View File
@@ -103,7 +103,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T)
# temporal pose # temporal pose
temporal_pose = modelV2.temporalPoseDEPRECATED temporal_pose = modelV2.temporalPose
temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist()
temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist()
temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist()
+3 -3
View File
@@ -25,7 +25,7 @@ from openpilot.sunnypilot.modeld.parse_model_outputs import Parser
from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan
from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd
from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext
@@ -62,7 +62,7 @@ class ModelState:
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
bundle = get_active_bundle() bundle = get_active_bundle()
overrides = {override.key: override.value for override in bundle.overrides} overrides = {override.key: override.value for override in bundle.overrides}
self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0")) self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2"))
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
model_paths = get_model_path() model_paths = get_model_path()
@@ -202,7 +202,7 @@ def main(demo=False):
cloudlog.info("modeld got CarParams: %s", CP.brand) cloudlog.info("modeld got CarParams: %s", CP.brand)
modeld_lagd = LagdToggle() modeld_lagd = ModeldLagd()
# Enable lagd support for sunnypilot modeld # Enable lagd support for sunnypilot modeld
long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS
+3 -3
View File
@@ -10,8 +10,8 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
ConfidenceClass = log.ModelDataV2.ConfidenceClass ConfidenceClass = log.ModelDataV2.ConfidenceClass
def get_curvature_from_output(output, vego, lat_action_t, mlsim): def get_curvature_from_output(output, vego, lat_action_t, current_generation=None):
if not mlsim: if current_generation != 11:
if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly
return float(desired_curv[0, 0]) return float(desired_curv[0, 0])
@@ -100,7 +100,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T)
# temporal pose # temporal pose
temporal_pose = modelV2.temporalPoseDEPRECATED temporal_pose = modelV2.temporalPose
if 'sim_pose' in net_output_data: if 'sim_pose' in net_output_data:
temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist()
temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist()
+6 -10
View File
@@ -23,7 +23,7 @@ from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFr
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd
from openpilot.sunnypilot.models.runners.helpers import get_model_runner from openpilot.sunnypilot.models.runners.helpers import get_model_runner
PROCESS_NAME = "selfdrive.modeld.modeld" PROCESS_NAME = "selfdrive.modeld.modeld"
@@ -54,10 +54,10 @@ class ModelState:
raise raise
model_bundle = get_active_bundle() model_bundle = get_active_bundle()
self.generation = model_bundle.generation if model_bundle is not None else None self.generation = model_bundle.generation
overrides = {override.key: override.value for override in model_bundle.overrides} overrides = {override.key: override.value for override in model_bundle.overrides}
self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0")) self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2"))
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
self.MIN_LAT_CONTROL_SPEED = 0.3 self.MIN_LAT_CONTROL_SPEED = 0.3
@@ -86,10 +86,6 @@ class ModelState:
self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1,
self.numpy_inputs['desire'].shape[2]) self.numpy_inputs['desire'].shape[2])
@property
def mlsim(self) -> bool:
return bool(self.generation is not None and self.generation >= 11)
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: inputs: dict[str, np.ndarray], 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 # Model decides when action is completed, so desire input is just a pulse triggered on rising edge
@@ -155,7 +151,7 @@ class ModelState:
self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:]
self.full_prev_desired_curv[0,-1,:] = outputs['desired_curvature'][0, :] self.full_prev_desired_curv[0,-1,:] = outputs['desired_curvature'][0, :]
self.numpy_inputs[input_name_prev][:] = self.full_prev_desired_curv[0, self.temporal_idxs] self.numpy_inputs[input_name_prev][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
if self.mlsim: if self.generation == 11:
self.numpy_inputs[input_name_prev][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs] self.numpy_inputs[input_name_prev][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs]
else: else:
length = outputs['desired_curvature'][0].size length = outputs['desired_curvature'][0].size
@@ -169,7 +165,7 @@ class ModelState:
action_t=long_action_t) action_t=long_action_t)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) 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) desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.generation)
if v_ego > self.MIN_LAT_CONTROL_SPEED: if v_ego > self.MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
else: else:
@@ -243,7 +239,7 @@ def main(demo=False):
cloudlog.info("modeld got CarParams: %s", CP.brand) cloudlog.info("modeld got CarParams: %s", CP.brand)
modeld_lagd = LagdToggle() modeld_lagd = ModeldLagd()
# TODO Move smooth seconds to action function # TODO Move smooth seconds to action function
long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS
@@ -1,6 +1,5 @@
import numpy as np import numpy as np
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
from openpilot.sunnypilot.models.helpers import get_active_bundle
def safe_exp(x, out=None): def safe_exp(x, out=None):
@@ -25,8 +24,6 @@ def softmax(x, axis=-1):
class Parser: class Parser:
def __init__(self, ignore_missing=False): def __init__(self, ignore_missing=False):
self.ignore_missing = ignore_missing self.ignore_missing = ignore_missing
model_bundle = get_active_bundle()
self.generation = model_bundle.generation if model_bundle is not None else None
def check_missing(self, outs, name): def check_missing(self, outs, name):
if name not in outs and not self.ignore_missing: if name not in outs and not self.ignore_missing:
@@ -91,64 +88,37 @@ class Parser:
outs[name] = pred_mu_final.reshape(final_shape) outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape)
def _parse_plan_mhp(self, outs):
self.parse_mdn('plan', outs, in_N=SplitModelConstants.PLAN_MHP_N, out_N=SplitModelConstants.PLAN_MHP_SELECTION,
out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.PLAN_WIDTH))
def parse_dynamic_outputs(self, outs: dict[str, np.ndarray]) -> None:
if 'lead' in outs:
if self.generation >= 12 and \
outs['lead'].shape[1] == 2 * SplitModelConstants.LEAD_MHP_SELECTION * SplitModelConstants.LEAD_TRAJ_LEN * SplitModelConstants.LEAD_WIDTH:
self.parse_mdn('lead', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.LEAD_MHP_SELECTION, SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH))
else:
self.parse_mdn('lead', outs, in_N=SplitModelConstants.LEAD_MHP_N, out_N=SplitModelConstants.LEAD_MHP_SELECTION,
out_shape=(SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH))
if 'plan' in outs:
if self.generation >= 12 and \
outs['plan'].shape[1] > 2 * SplitModelConstants.PLAN_WIDTH * SplitModelConstants.IDX_N:
self._parse_plan_mhp(outs)
elif self.generation >= 12:
self.parse_mdn('plan', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH))
else:
self._parse_plan_mhp(outs)
def split_outputs(self, outs: dict[str, np.ndarray]) -> None: def split_outputs(self, outs: dict[str, np.ndarray]) -> None:
if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,))
if 'desire_pred' in outs:
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH))
if 'desire_state' in outs:
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,))
if 'lane_lines' in outs: if 'lane_lines' in outs:
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, self.parse_mdn('lane_lines', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
if 'lane_lines_prob' in outs:
self.parse_binary_crossentropy('lane_lines_prob', outs)
if 'lead_prob' in outs:
self.parse_binary_crossentropy('lead_prob', outs)
if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'meta' in outs:
self.parse_binary_crossentropy('meta', outs)
if 'road_edges' in outs:
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, self.parse_mdn('road_edges', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
if 'sim_pose' in outs: self.parse_mdn('lead', outs, in_N=SplitModelConstants.LEAD_MHP_N, out_N=SplitModelConstants.LEAD_MHP_SELECTION,
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) out_shape=(SplitModelConstants.LEAD_TRAJ_LEN,SplitModelConstants.LEAD_WIDTH))
if 'sim_pose' in outs:
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob']:
self.parse_binary_crossentropy(k, outs)
def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.WIDE_FROM_DEVICE_WIDTH,))
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
self.parse_dynamic_outputs(outs)
self.split_outputs(outs) self.split_outputs(outs)
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH))
self.parse_binary_crossentropy('meta', outs)
return outs return outs
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_dynamic_outputs(outs) self.parse_mdn('plan', outs, in_N=SplitModelConstants.PLAN_MHP_N, out_N=SplitModelConstants.PLAN_MHP_SELECTION,
out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.PLAN_WIDTH))
self.split_outputs(outs) self.split_outputs(outs)
if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,))
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,))
return outs return outs
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
+1 -1
View File
@@ -115,7 +115,7 @@ class ModelCache:
class ModelFetcher: class ModelFetcher:
"""Handles fetching and caching of model data from remote source""" """Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v5.json" MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v4.json"
def __init__(self, params: Params): def __init__(self, params: Params):
self.params = params self.params = params
+2 -2
View File
@@ -19,8 +19,8 @@ from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from pathlib import Path from pathlib import Path
CURRENT_SELECTOR_VERSION = 1 CURRENT_SELECTOR_VERSION = 6
REQUIRED_MIN_SELECTOR_VERSION = 1 REQUIRED_MIN_SELECTOR_VERSION = 5
USE_ONNX = os.getenv('USE_ONNX', PC) USE_ONNX = os.getenv('USE_ONNX', PC)
-29
View File
@@ -178,10 +178,6 @@ class ModelManagerSP:
finally: finally:
self.params.put("ModelManager_DownloadIndex", "") self.params.put("ModelManager_DownloadIndex", "")
if self.params.get("ModelManager_ClearCache", block=False, encoding="utf-8"):
self.clear_model_cache()
self.params.remove("ModelManager_ClearCache")
self._report_status() self._report_status()
rk.keep_time() rk.keep_time()
@@ -189,31 +185,6 @@ class ModelManagerSP:
cloudlog.exception(f"Error in main thread: {str(e)}") cloudlog.exception(f"Error in main thread: {str(e)}")
rk.keep_time() rk.keep_time()
def clear_model_cache(self) -> None:
"""
Clears the model cache directory of all files except those in the active model bundle.
"""
# Get list of files used by active model bundle
active_files = []
if self.active_bundle is not None: # When the default model is active
for model in self.active_bundle.models:
if hasattr(model, 'artifact') and model.artifact.fileName:
active_files.append(model.artifact.fileName)
if hasattr(model, 'metadata') and model.metadata.fileName:
active_files.append(model.metadata.fileName)
# Remove all files except active ones
model_dir = Paths.model_root()
try:
for filename in os.listdir(model_dir):
if filename not in active_files:
file_path = os.path.join(model_dir, filename)
if os.path.isfile(file_path):
os.remove(file_path)
cloudlog.info("Model cache cleared, keeping active model files")
except Exception as e:
cloudlog.exception(f"Error clearing model cache: {str(e)}")
def main(): def main():
ModelManagerSP().main_thread() ModelManagerSP().main_thread()
+26
View File
@@ -0,0 +1,26 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
class ModeldLagd:
def __init__(self):
self.params = Params()
def lagd_main(self, CP, sm, model):
if self.params.get_bool("LagdToggle"):
lateral_delay = sm["liveDelay"].lateralDelay
lat_smooth = model.LAT_SMOOTH_SECONDS
result = lateral_delay + lat_smooth
cloudlog.debug(f"LAGD USING LIVE DELAY: {lateral_delay:.3f} + {lat_smooth:.3f} = {result:.3f}")
return result
steer_actuator_delay = CP.steerActuatorDelay
lat_smooth = model.LAT_SMOOTH_SECONDS
result = (steer_actuator_delay + 0.2) + lat_smooth
cloudlog.debug(f"LAGD USING STEER ACTUATOR: {steer_actuator_delay:.3f} + 0.2 + {lat_smooth:.3f} = {result:.3f}")
return result
+1 -1
View File
@@ -1 +1 @@
61bb1f3077ef8219a44c8c627d5345d1a96e7f859b850bed6215eec91090becb 71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94
@@ -1,17 +1,25 @@
class WMACConstants: class WMACConstants:
# Lead detection parameters LEAD_WINDOW_SIZE = 5
LEAD_WINDOW_SIZE = 6 # Stable detection window LEAD_PROB = 0.5
LEAD_PROB = 0.45 # Balanced threshold for lead detection
# Slow down detection parameters SLOW_DOWN_WINDOW_SIZE = 5
SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable SLOW_DOWN_PROB = 0.6
SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios
# Optimized slow down distance curve - smooth and progressive
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.] SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.] SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.]
# Slowness detection parameters SLOWNESS_WINDOW_SIZE = 12
SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection SLOWNESS_PROB = 0.5
SLOWNESS_PROB = 0.55 # Clear threshold for slowness SLOWNESS_CRUISE_OFFSET = 1.05
SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset
DANGEROUS_TTC_WINDOW_SIZE = 3
DANGEROUS_TTC = 2.3
MPC_FCW_WINDOW_SIZE = 10
MPC_FCW_PROB = 0.5
class SNG_State:
off = 0
stopped = 1
going = 2
+280 -278
View File
@@ -1,133 +1,88 @@
""" # The MIT License
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors. #
# Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Version = 2025-1-18
This file is part of sunnypilot and is licensed under the MIT License. import numpy as np
See the LICENSE.md file in the root directory for more details.
"""
# Version = 2025-6-30
from cereal import messaging from cereal import messaging
from opendbc.car import structs from opendbc.car import structs
from numpy import interp from numpy import interp
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State
from typing import Literal
# d-e2e, from modeldata.h # d-e2e, from modeldata.h
TRAJECTORY_SIZE = 33 TRAJECTORY_SIZE = 33
SET_MODE_TIMEOUT = 15
# Define the valid mode types HIGHWAY_CRUISE_KPH = 70
ModeType = Literal['acc', 'blended']
STOP_AND_GO_FRAME = 60
SET_MODE_TIMEOUT = 10
V_ACC_MIN = 9.72
class SmoothKalmanFilter: class GenericMovingAverageCalculator:
"""Enhanced Kalman filter with smoothing for stable decision making.""" def __init__(self, window_size):
self.window_size = window_size
self.data = []
self.total = 0
def __init__(self, initial_value=0, measurement_noise=0.1, process_noise=0.01, def add_data(self, value: float) -> None:
alpha=1.0, smoothing_factor=0.85): if len(self.data) == self.window_size:
self.x = initial_value self.total -= self.data.pop(0)
self.P = 1.0 self.data.append(value)
self.R = measurement_noise self.total += value
self.Q = process_noise
self.alpha = alpha
self.smoothing_factor = smoothing_factor
self.initialized = False
self.history = []
self.max_history = 10
self.confidence = 0.0
def add_data(self, measurement): def get_moving_average(self) -> float | None:
if len(self.history) >= self.max_history: return None if len(self.data) == 0 else self.total / len(self.data)
self.history.pop(0)
self.history.append(measurement)
if not self.initialized: def reset_data(self) -> None:
self.x = measurement self.data = []
self.initialized = True self.total = 0
self.confidence = 0.1
return
self.P = self.alpha * self.P + self.Q
K = self.P / (self.P + self.R)
effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1
innovation = measurement - self.x
self.x = self.x + effective_K * innovation
self.P = (1 - effective_K) * self.P
if abs(innovation) < 0.1:
self.confidence = min(1.0, self.confidence + 0.05)
else:
self.confidence = max(0.1, self.confidence - 0.02)
def get_value(self):
return self.x if self.initialized else None
def get_confidence(self):
return self.confidence
def reset_data(self):
self.initialized = False
self.history = []
self.confidence = 0.0
class ModeTransitionManager: class WeightedMovingAverageCalculator:
"""Manages smooth transitions between driving modes with hysteresis.""" def __init__(self, window_size):
self.window_size = window_size
self.data = []
self.weights = np.linspace(1, 3, window_size) # Linear weights, adjust as needed
def __init__(self): def add_data(self, value: float) -> None:
self.current_mode: ModeType = 'acc' if len(self.data) == self.window_size:
self.mode_confidence = {'acc': 1.0, 'blended': 0.0} self.data.pop(0)
self.transition_timeout = 0 self.data.append(value)
self.min_mode_duration = 10
self.mode_duration = 0
self.emergency_override = False
def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False): def get_weighted_average(self) -> float | None:
# Emergency override for critical situations (stops, collisions) if len(self.data) == 0:
if emergency: return None
self.emergency_override = True weighted_sum: float = float(np.dot(self.data, self.weights[-len(self.data):]))
self.current_mode = mode weight_total: float = float(np.sum(self.weights[-len(self.data):]))
self.transition_timeout = SET_MODE_TIMEOUT return weighted_sum / weight_total
self.mode_duration = 0
return
self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence) def reset_data(self) -> None:
for m in self.mode_confidence: self.data = []
if m != mode:
self.mode_confidence[m] = max(0.0, self.mode_confidence[m] - 0.05)
# Require minimum duration in current mode (unless emergency)
if self.mode_duration < self.min_mode_duration and not self.emergency_override:
return
# Hysteresis: higher threshold for mode changes
confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response
if self.mode_confidence[mode] > confidence_threshold:
if mode != self.current_mode and self.transition_timeout == 0:
self.transition_timeout = SET_MODE_TIMEOUT
self.current_mode = mode
self.mode_duration = 0
def update(self):
if self.transition_timeout > 0:
self.transition_timeout -= 1
self.mode_duration += 1
# Reset emergency override after some time
if self.emergency_override and self.mode_duration > 20:
self.emergency_override = False
# Gradual confidence decay
for mode in self.mode_confidence:
self.mode_confidence[mode] *= 0.98
def get_mode(self) -> ModeType:
return self.current_mode
class DynamicExperimentalController: class DynamicExperimentalController:
@@ -137,59 +92,51 @@ class DynamicExperimentalController:
self._params = params or Params() self._params = params or Params()
self._enabled: bool = self._params.get_bool("DynamicExperimentalControl") self._enabled: bool = self._params.get_bool("DynamicExperimentalControl")
self._active: bool = False self._active: bool = False
self._mode: str = 'acc'
self._frame: int = 0 self._frame: int = 0
self._urgency = 0.0
self._mode_manager = ModeTransitionManager() # Use weighted moving average for filtering leads
self._lead_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.LEAD_WINDOW_SIZE)
# Smooth filters for stable decision making with faster response for critical scenarios
self._lead_filter = SmoothKalmanFilter(
measurement_noise=0.15,
process_noise=0.05,
alpha=1.02,
smoothing_factor=0.8
)
self._slow_down_filter = SmoothKalmanFilter(
measurement_noise=0.1,
process_noise=0.1,
alpha=1.05,
smoothing_factor=0.7
)
self._slowness_filter = SmoothKalmanFilter(
measurement_noise=0.1,
process_noise=0.06,
alpha=1.015,
smoothing_factor=0.92
)
self._mpc_fcw_filter = SmoothKalmanFilter(
measurement_noise=0.2,
process_noise=0.1,
alpha=1.1,
smoothing_factor=0.5
)
self._has_lead_filtered = False self._has_lead_filtered = False
self._has_slow_down = False self._has_lead_filtered_prev = False
self._has_slowness = False
self._has_mpc_fcw = False self._slow_down_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOW_DOWN_WINDOW_SIZE)
self._v_ego_kph = 0.0 self._has_slow_down: bool = False
self._v_cruise_kph = 0.0 self._slow_down_confidence: float = 0.0
self._has_blinkers = False
self._slowness_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOWNESS_WINDOW_SIZE)
self._has_slowness: bool = False
self._has_nav_instruction = False
self._dangerous_ttc_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.DANGEROUS_TTC_WINDOW_SIZE)
self._has_dangerous_ttc: bool = False
self._v_ego_kph = 0.
self._v_cruise_kph = 0.
self._has_lead = False
self._has_standstill = False self._has_standstill = False
self._has_standstill_prev = False
self._sng_transit_frame = 0
self._sng_state = SNG_State.off
self._mpc_fcw_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.MPC_FCW_WINDOW_SIZE)
self._has_mpc_fcw: bool = False
self._mpc_fcw_crash_cnt = 0 self._mpc_fcw_crash_cnt = 0
self._standstill_count = 0
# debug self._set_mode_timeout = 0
self._endpoint_x = float('inf')
self._expected_distance = 0.0
self._trajectory_valid = False
def _read_params(self) -> None: def _read_params(self) -> None:
if self._frame % int(1. / DT_MDL) == 0: if self._frame % int(1. / DT_MDL) == 0:
self._enabled = self._params.get_bool("DynamicExperimentalControl") self._enabled = self._params.get_bool("DynamicExperimentalControl")
def mode(self) -> str: def mode(self) -> str:
return self._mode_manager.get_mode() return str(self._mode)
def enabled(self) -> bool: def enabled(self) -> bool:
return self._enabled return self._enabled
@@ -197,9 +144,46 @@ class DynamicExperimentalController:
def active(self) -> bool: def active(self) -> bool:
return self._active return self._active
def set_mpc_fcw_crash_cnt(self) -> None: @staticmethod
"""Set MPC FCW crash count""" def _anomaly_detection(recent_data: list[float], threshold: float = 2.0, context_check: bool = True) -> bool:
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt """
Basic anomaly detection using standard deviation.
"""
if len(recent_data) < 5:
return False
mean: float = float(np.mean(recent_data))
std_dev: float = float(np.std(recent_data))
anomaly: bool = bool(recent_data[-1] > mean + threshold * std_dev)
# Context check to ensure repeated anomaly
if context_check:
return np.count_nonzero(np.array(recent_data) > mean + threshold * std_dev) > 1
return anomaly
def _adaptive_slowdown_threshold(self) -> float:
"""
Adapts the slow-down threshold based on vehicle speed and recent behavior.
"""
slowdown_scaling_factor: float = (1.0 + 0.03 * np.log(1 + len(self._slow_down_gmac.data)))
adaptive_threshold: float = float(
interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST) * slowdown_scaling_factor
)
return adaptive_threshold
def _smoothed_lead_detection(self, lead_prob: float, smoothing_factor: float = 0.2):
"""
Smoothing the lead detection to avoid erratic behavior.
"""
lead_filtering: float = (1 - smoothing_factor) * self._has_lead_filtered + smoothing_factor * lead_prob
return lead_filtering > WMACConstants.LEAD_PROB
def _adaptive_lead_prob_threshold(self) -> float:
"""
Adapts lead probability threshold based on driving conditions.
"""
if self._v_ego_kph > HIGHWAY_CRUISE_KPH:
return float(WMACConstants.LEAD_PROB + 0.1) # Increase the threshold on highways
return float(WMACConstants.LEAD_PROB)
def _update_calculations(self, sm: messaging.SubMaster) -> None: def _update_calculations(self, sm: messaging.SubMaster) -> None:
car_state = sm['carState'] car_state = sm['carState']
@@ -208,168 +192,186 @@ class DynamicExperimentalController:
self._v_ego_kph = car_state.vEgo * 3.6 self._v_ego_kph = car_state.vEgo * 3.6
self._v_cruise_kph = car_state.vCruise self._v_cruise_kph = car_state.vCruise
self._has_lead = lead_one.status
self._has_standstill = car_state.standstill self._has_standstill = car_state.standstill
# standstill detection # fcw detection
if self._has_standstill: self._mpc_fcw_gmac.add_data(self._mpc_fcw_crash_cnt > 0)
self._standstill_count = min(20, self._standstill_count + 1) if _mpc_fcw_weighted_average := self._mpc_fcw_gmac.get_weighted_average():
self._has_mpc_fcw = _mpc_fcw_weighted_average > WMACConstants.MPC_FCW_PROB
else: else:
self._standstill_count = max(0, self._standstill_count - 1) self._has_mpc_fcw = False
# Lead detection # nav enable detection
self._lead_filter.add_data(float(lead_one.status)) # self._has_nav_instruction = md.navEnabledDEPRECATED and maneuver_distance / max(car_state.vEgo, 1) < 13
lead_value = self._lead_filter.get_value() or 0.0
self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB
# MPC FCW detection # lead detection with smoothing
fcw_filtered_value = self._mpc_fcw_filter.get_value() or 0.0 self._lead_gmac.add_data(lead_one.status)
self._mpc_fcw_filter.add_data(float(self._mpc_fcw_crash_cnt > 0)) self._has_lead_filtered = (self._lead_gmac.get_weighted_average() or -1.) > WMACConstants.LEAD_PROB
self._has_mpc_fcw = fcw_filtered_value > 0.5 #lead_prob = self._lead_gmac.get_weighted_average() or 0
#self._has_lead_filtered = self._smoothed_lead_detection(lead_prob)
# Slow down detection # adaptive slow down detection
self._calculate_slow_down(md) adaptive_threshold = self._adaptive_slowdown_threshold()
slow_down_trigger = len(md.orientation.x) == len(md.position.x) == TRAJECTORY_SIZE and md.position.x[TRAJECTORY_SIZE - 1] < adaptive_threshold
self._slow_down_gmac.add_data(slow_down_trigger)
if _has_slow_down_weighted_average := self._slow_down_gmac.get_weighted_average():
self._has_slow_down = _has_slow_down_weighted_average > WMACConstants.SLOW_DOWN_PROB
self._slow_down_confidence = _has_slow_down_weighted_average # Store confidence level
else:
self._has_slow_down = False
self._slow_down_confidence = 0.0 # No confidence if no slowdown
# Slowness detection # anomaly detection for slow down events
if not (self._standstill_count > 5) and not self._has_slow_down: if self._anomaly_detection(self._slow_down_gmac.data):
current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET)) self._slow_down_confidence *= 0.85 # Reduce confidence
self._slowness_filter.add_data(current_slowness) self._has_slow_down = self._slow_down_confidence > WMACConstants.SLOW_DOWN_PROB
slowness_value = self._slowness_filter.get_value() or 0.0
# Hysteresis for slowness # blinker detection
threshold = WMACConstants.SLOWNESS_PROB * (0.8 if self._has_slowness else 1.1) self._has_blinkers = car_state.leftBlinker or car_state.rightBlinker
self._has_slowness = slowness_value > threshold
def _calculate_slow_down(self, md): # sng detection
"""Calculate urgency based on trajectory endpoint vs expected distance.""" if self._has_standstill:
self._sng_state = SNG_State.stopped
self._sng_transit_frame = 0
else:
if self._sng_transit_frame == 0:
if self._sng_state == SNG_State.stopped:
self._sng_state = SNG_State.going
self._sng_transit_frame = STOP_AND_GO_FRAME
elif self._sng_state == SNG_State.going:
self._sng_state = SNG_State.off
elif self._sng_transit_frame > 0:
self._sng_transit_frame -= 1
# Reset to safe defaults # slowness detection
urgency = 0.0 if not self._has_standstill:
self._endpoint_x = float('inf') self._slowness_gmac.add_data(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
self._trajectory_valid = False if _slowness_weighted_average := self._slowness_gmac.get_weighted_average():
self._has_slowness = _slowness_weighted_average > WMACConstants.SLOWNESS_PROB
else:
self._has_slowness = False
#Require exact trajectory size # dangerous TTC detection
position_valid = len(md.position.x) == TRAJECTORY_SIZE if not self._has_lead_filtered and self._has_lead_filtered_prev:
orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE self._dangerous_ttc_gmac.reset_data()
self._has_dangerous_ttc = False
if not (position_valid and orientation_valid): if self._has_lead and car_state.vEgo >= 0.01:
# Invalid trajectory - this itself might indicate a stop scenario self._dangerous_ttc_gmac.add_data(lead_one.dRel / car_state.vEgo)
# Apply moderate urgency for incomplete trajectories at speed
if self._v_ego_kph > 20.0:
urgency = 0.3
self._slow_down_filter.add_data(urgency) if _dangerous_ttc_weighted_average := self._dangerous_ttc_gmac.get_weighted_average():
urgency_filtered = self._slow_down_filter.get_value() or 0.0 self._has_dangerous_ttc = _dangerous_ttc_weighted_average <= WMACConstants.DANGEROUS_TTC
self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB else:
self._urgency = urgency_filtered self._has_dangerous_ttc = False
return
# We have a valid full trajectory # keep prev values
self._trajectory_valid = True self._has_standstill_prev = self._has_standstill
self._has_lead_filtered_prev = self._has_lead_filtered
# Use the exact endpoint (33rd point, index 32)
endpoint_x = md.position.x[TRAJECTORY_SIZE - 1]
self._endpoint_x = endpoint_x
# Get expected distance based on current speed using tuned constants
expected_distance = interp(self._v_ego_kph,
WMACConstants.SLOW_DOWN_BP,
WMACConstants.SLOW_DOWN_DIST)
self._expected_distance = expected_distance
# Calculate urgency based on trajectory shortage
if endpoint_x < expected_distance:
shortage = expected_distance - endpoint_x
shortage_ratio = shortage / expected_distance
# Base urgency on shortage ratio
urgency = min(1.0, shortage_ratio * 2.0)
# Increase urgency for very short trajectories (imminent stops)
critical_distance = expected_distance * 0.3
if endpoint_x < critical_distance:
urgency = min(1.0, urgency * 2.0)
# Speed-based urgency adjustment
if self._v_ego_kph > 25.0:
speed_factor = 1.0 + (self._v_ego_kph - 25.0) / 80.0
urgency = min(1.0, urgency * speed_factor)
# Apply filtering but with less smoothing for stops
self._slow_down_filter.add_data(urgency)
urgency_filtered = self._slow_down_filter.get_value() or 0.0
# Update state with lower threshold for better stop detection
self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8)
self._urgency = urgency_filtered
def _radarless_mode(self) -> None: def _radarless_mode(self) -> None:
"""Radarless mode decision logic with emergency handling.""" # when mpc fcw crash prob is high
# use blended to slow down quickly
# EMERGENCY: MPC FCW - immediate blended mode
if self._has_mpc_fcw: if self._has_mpc_fcw:
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) self._set_mode('blended')
return return
# Standstill: use blended # Nav enabled and distance to upcoming turning is 300 or below
if self._standstill_count > 3: # if self._has_nav_instruction:
self._mode_manager.request_mode('blended', confidence=0.9) # self._set_mode('blended')
# return
# when blinker is on and speed is driving below V_ACC_MIN: blended
# we don't want it to switch mode at higher speed, blended may trigger hard brake
# if self._has_blinkers and self._v_ego_kph < V_ACC_MIN:
# self._set_mode('blended')
# return
# when at highway cruise and SNG: blended
# ensuring blended mode is used because acc is bad at catching SNG lead car
# especially those who accel very fast and then brake very hard.
# if self._sng_state == SNG_State.going and self._v_cruise_kph >= V_ACC_MIN:
# self._set_mode('blended')
# return
# when standstill: blended
# in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light.
if self._has_standstill:
self._set_mode('blended')
return return
# Slow down scenarios: emergency for high urgency, normal for lower urgency # when detecting slow down scenario: blended
# e.g. traffic light, curve, stop sign etc.
if self._has_slow_down: if self._has_slow_down:
if self._urgency > 0.7: self._set_mode('blended')
# Emergency: immediate blended mode for high urgency stops
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
else:
# Normal: blended with urgency-based confidence
confidence = min(1.0, self._urgency * 1.5)
self._mode_manager.request_mode('blended', confidence=confidence)
return return
# Driving slow: use ACC (but not if actively slowing down) # when detecting lead slow down: blended
if self._has_slowness and not self._has_slow_down: # use blended for higher braking capability
self._mode_manager.request_mode('acc', confidence=0.8) if self._has_dangerous_ttc:
self._set_mode('blended')
return return
# Default: ACC # car driving at speed lower than set speed: acc
self._mode_manager.request_mode('acc', confidence=0.7) if self._has_slowness:
self._set_mode('acc')
return
self._set_mode('acc')
def _radar_mode(self) -> None: def _radar_mode(self) -> None:
"""Radar mode with emergency handling.""" # when mpc fcw crash prob is high
# use blended to slow down quickly
# EMERGENCY: MPC FCW - immediate blended mode
if self._has_mpc_fcw: if self._has_mpc_fcw:
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) self._set_mode('blended')
return return
# If lead detected and not in standstill: always use ACC # If there is a filtered lead, the vehicle is not in standstill, and the lead vehicle's yRel meets the condition,
if self._has_lead_filtered and not (self._standstill_count > 3): if self._has_lead_filtered and not self._has_standstill:
self._mode_manager.request_mode('acc', confidence=1.0) self._set_mode('acc')
return return
# Slow down scenarios: emergency for high urgency, normal for lower urgency # when blinker is on and speed is driving below V_ACC_MIN: blended
# we don't want it to switch mode at higher speed, blended may trigger hard brake
# if self._has_blinkers and self._v_ego_kph < V_ACC_MIN:
# self._set_mode('blended')
# return
# when standstill: blended
# in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light.
if self._has_standstill:
self._set_mode('blended')
return
# when detecting slow down scenario: blended
# e.g. traffic light, curve, stop sign etc.
if self._has_slow_down: if self._has_slow_down:
if self._urgency > 0.7: self._set_mode('blended')
# Emergency: immediate blended mode for high urgency stops
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
else:
# Normal: blended with urgency-based confidence
confidence = min(1.0, self._urgency * 1.3)
self._mode_manager.request_mode('blended', confidence=confidence)
return return
# Standstill: use blended # car driving at speed lower than set speed: acc
if self._standstill_count > 3: if self._has_slowness:
self._mode_manager.request_mode('blended', confidence=0.9) self._set_mode('acc')
return return
# Driving slow: use ACC (but not if actively slowing down) # Nav enabled and distance to upcoming turning is 300 or below
if self._has_slowness and not self._has_slow_down: # if self._has_nav_instruction:
self._mode_manager.request_mode('acc', confidence=0.8) # self._set_mode('blended')
return # return
# Default: ACC self._set_mode('acc')
self._mode_manager.request_mode('acc', confidence=0.7)
def set_mpc_fcw_crash_cnt(self) -> None:
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
def _set_mode(self, mode: str) -> None:
if self._set_mode_timeout == 0:
self._mode = mode
if mode == 'blended':
self._set_mode_timeout = SET_MODE_TIMEOUT
if self._set_mode_timeout > 0:
self._set_mode_timeout -= 1
def update(self, sm: messaging.SubMaster) -> None: def update(self, sm: messaging.SubMaster) -> None:
self._read_params() self._read_params()
@@ -383,6 +385,6 @@ class DynamicExperimentalController:
else: else:
self._radar_mode() self._radar_mode()
self._mode_manager.update()
self._active = sm['selfdriveState'].experimentalMode and self._enabled self._active = sm['selfdriveState'].experimentalMode and self._enabled
self._frame += 1 self._frame += 1
@@ -1,94 +1,248 @@
import pytest import pytest
import numpy as np
from openpilot.common.params import Params
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController, TRAJECTORY_SIZE, STOP_AND_GO_FRAME
class MockLeadOne: class MockInterp:
def __init__(self, status=0.0): def __call__(self, x, xp, fp):
self.status = status return np.interp(x, xp, fp)
class MockRadarState:
def __init__(self, status=0.0):
self.leadOne = MockLeadOne(status=status)
class MockCarState: class MockCarState:
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False): def __init__(self, v_ego=0., standstill=False, left_blinker=False, right_blinker=False):
self.vEgo = vEgo self.vEgo = v_ego
self.vCruise = vCruise
self.standstill = standstill self.standstill = standstill
self.leftBlinker = left_blinker
self.rightBlinker = right_blinker
class MockLeadOne:
def __init__(self, status=False, d_rel=0):
self.status = status
self.dRel = d_rel
class MockModelData: class MockModelData:
def __init__(self, valid=True): def __init__(self, x_vals=None, positions=None):
size = 33 if valid else 10 # incomplete if invalid self.orientation = type('Orientation', (), {'x': x_vals})()
self.position = type("Pos", (), {"x": [0.0] * size})() self.position = type('Position', (), {'x': positions})()
self.orientation = type("Ori", (), {"x": [0.0] * size})()
class MockSelfDriveState: class MockControlState:
def __init__(self, experimentalMode=False): def __init__(self, v_cruise=0):
self.experimentalMode = experimentalMode self.vCruise = v_cruise
class MockParams:
def get_bool(self, name):
return True
@pytest.fixture @pytest.fixture
def default_sm(): def interp(monkeypatch):
sm = { mock_interp = MockInterp()
'carState': MockCarState(vEgo=10.0, vCruise=20.0), monkeypatch.setattr('numpy.interp', mock_interp)
'radarState': MockRadarState(status=1.0), return mock_interp
'modelV2': MockModelData(valid=True),
'selfdriveState': MockSelfDriveState(experimentalMode=True),
}
return sm
@pytest.fixture @pytest.fixture
def mock_cp(): def controller(interp):
class CP: params = Params()
radarUnavailable = False params.put_bool("DynamicExperimentalControl", True)
return CP() return DynamicExperimentalController()
@pytest.fixture def test_initial_state(controller):
def mock_mpc(): """Test initial state of the controller"""
class MPC: assert controller._mode == 'acc'
crash_cnt = 0 assert not controller._has_lead
return MPC() assert not controller._has_standstill
assert controller._sng_state == SNG_State.off
assert not controller._has_lead_filtered
assert not controller._has_slow_down
assert not controller._has_dangerous_ttc
assert not controller._has_mpc_fcw
# Fake Kalman Filter that always returns a given value @pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
class FakeKalman: def test_standstill_detection(controller, has_radar):
def __init__(self, value=1.0): """Test standstill detection and state transitions"""
self.value = value car_state = MockCarState(standstill=True)
def add_data(self, v): pass lead_one = MockLeadOne()
def get_value(self): return self.value md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
def get_confidence(self): return 1.0 controls_state = MockControlState()
def reset_data(self): pass
def test_initial_mode_is_acc(mock_cp, mock_mpc): # Test transition to standstill
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller.mode() == "acc" assert controller._sng_state == SNG_State.stopped
assert controller.get_mpc_mode() == 'blended'
def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm): # Test transition from standstill to moving
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) car_state.standstill = False
default_sm['carState'].standstill = True controller.update(not has_radar, car_state, lead_one, md, controls_state)
for _ in range(10): assert controller._sng_state == SNG_State.going
controller.update(default_sm)
assert controller.mode() == "blended"
def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm): # Test complete transition to normal driving
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) for _ in range(STOP_AND_GO_FRAME + 1):
mock_mpc.crash_cnt = 1 # simulate FCW controller.update(not has_radar, car_state, lead_one, md, controls_state)
for _ in range(2): assert controller._sng_state == SNG_State.off
controller.update(default_sm)
assert controller.mode() == "blended"
def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm): @pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
mock_cp.radarUnavailable = True def test_lead_detection(controller, has_radar):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) """Test lead vehicle detection and filtering"""
car_state = MockCarState(v_ego=20) # 72 kph
lead_one = MockLeadOne(status=True, d_rel=50) # Safe distance
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
controls_state = MockControlState(v_cruise=72)
# Force conditions to simulate slowdown # Let moving average stabilize
controller._slow_down_filter = FakeKalman(value=1.0) # Ensure urgency triggers slowdown for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
controller._v_ego_kph = 35.0 controller.update(not has_radar, car_state, lead_one, md, controls_state)
default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory
for _ in range(3): assert controller._has_lead_filtered
controller.update(default_sm) expected_mode = 'acc' if has_radar else 'blended'
assert controller.get_mpc_mode() == expected_mode
assert controller.mode() == "blended" # Test lead loss detection
lead_one.status = False
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert not controller._has_lead_filtered
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
def test_slow_down_detection(controller, has_radar):
"""Test slow down detection based on trajectory"""
car_state = MockCarState(v_ego=10/3.6) # 10 kph
lead_one = MockLeadOne()
x_vals = [0] * TRAJECTORY_SIZE
positions = [20] * TRAJECTORY_SIZE # Position within slow down threshold
md = MockModelData(x_vals=x_vals, positions=positions)
controls_state = MockControlState(v_cruise=30)
# Test slow down detection
for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller._has_slow_down
assert controller.get_mpc_mode() == 'blended'
# Test slow down recovery
positions = [200] * TRAJECTORY_SIZE # Position outside slow down threshold
md = MockModelData(x_vals=x_vals, positions=positions)
for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert not controller._has_slow_down
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
def test_dangerous_ttc_detection(controller, has_radar):
"""Test Time-To-Collision detection and handling"""
car_state = MockCarState(v_ego=10) # 36 kph
lead_one = MockLeadOne(status=True)
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
controls_state = MockControlState(v_cruise=36)
# First establish normal conditions with lead
lead_one.dRel = 100 # Safe distance
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): # First establish lead detection
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller._has_lead_filtered # Verify lead is detected
# Now test dangerous TTC detection
lead_one.dRel = 10 # 10m distance - should trigger dangerous TTC
# TTC = dRel/vEgo = 10/10 = 1s (which is less than DANGEROUS_TTC = 2.3s)
# Need to update multiple times to allow the weighted average to stabilize
for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller._has_dangerous_ttc, "TTC of 1s should be considered dangerous"
expected_mode = 'acc' if has_radar else 'blended'
assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC"
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
def test_mode_transitions(controller, has_radar):
"""Test comprehensive mode transitions under different conditions"""
# Initialize with normal driving conditions
car_state = MockCarState(v_ego=25) # 90 kph
lead_one = MockLeadOne(status=False)
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[200] * TRAJECTORY_SIZE)
controls_state = MockControlState(v_cruise=100)
def stabilize_filters():
"""Helper to let all moving averages stabilize"""
for _ in range(max(WMACConstants.LEAD_WINDOW_SIZE, WMACConstants.SLOW_DOWN_WINDOW_SIZE,
WMACConstants.DANGEROUS_TTC_WINDOW_SIZE, WMACConstants.MPC_FCW_WINDOW_SIZE) + 1):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
# Test 1: Normal driving -> ACC mode
stabilize_filters()
assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode under normal driving conditions"
# Test 2: Standstill -> Blended mode
car_state.standstill = True
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller.get_mpc_mode() == 'blended', "Should be in blended mode during standstill"
# Test 3: Lead car appears -> ACC mode
car_state = MockCarState(v_ego=20) # Reset car state
lead_one.status = True
lead_one.dRel = 50 # Safe distance
stabilize_filters()
assert not controller._has_dangerous_ttc, "Should not have dangerous TTC"
assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode with safe lead distance"
# Test 4: Dangerous TTC -> Blended mode
car_state = MockCarState(v_ego=20) # 72 kph
lead_one.status = True
lead_one.dRel = 50 # First establish normal lead detection
# First establish lead detection
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller._has_lead_filtered # Verify lead is detected
# Now create dangerous TTC condition
lead_one.dRel = 20 # This creates a TTC of 1s, well below DANGEROUS_TTC
for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller._has_dangerous_ttc, "Should detect dangerous TTC condition"
expected_mode = 'acc' if has_radar else 'blended'
assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC"
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
def test_mpc_fcw_handling(controller, has_radar):
"""Test MPC FCW crash count handling and mode transitions"""
car_state = MockCarState(v_ego=20)
lead_one = MockLeadOne()
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
controls_state = MockControlState(v_cruise=72)
# Test FCW activation
controller.set_mpc_fcw_crash_cnt(5)
for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert controller._has_mpc_fcw
assert controller.get_mpc_mode() == 'blended'
# Test FCW recovery
controller.set_mpc_fcw_crash_cnt(0)
for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1):
controller.update(not has_radar, car_state, lead_one, md, controls_state)
assert not controller._has_mpc_fcw
def test_radar_unavailable_handling(controller):
"""Test behavior transitions between radar available and unavailable states"""
car_state = MockCarState(v_ego=27.78) # 100 kph
lead_one = MockLeadOne(status=True, d_rel=50)
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
controls_state = MockControlState(v_cruise=100)
# Test with radar available
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
controller.update(False, car_state, lead_one, md, controls_state)
radar_mode = controller.get_mpc_mode()
# Test with radar unavailable
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
controller.update(True, car_state, lead_one, md, controls_state)
radarless_mode = controller.get_mpc_mode()
assert radar_mode is not None
assert radarless_mode is not None
@@ -10,8 +10,6 @@ import numpy as np
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
LAT_PLAN_MIN_IDX = 5 LAT_PLAN_MIN_IDX = 5
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
@@ -43,13 +41,11 @@ def get_lookahead_value(future_vals, current_val):
return min_val return min_val
class LatControlTorqueExtBase(LagdToggle): class LatControlTorqueExtBase:
def __init__(self, lac_torque, CP, CP_SP): def __init__(self, lac_torque, CP, CP_SP):
LagdToggle.__init__(self)
self.model_v2 = None self.model_v2 = None
self.model_valid = False self.model_valid = False
self.torque_params = lac_torque.torque_params self.use_steering_angle = lac_torque.use_steering_angle
self.use_steering_angle = lac_torque.torque_params.useSteeringAngle
self.actual_lateral_jerk: float = 0.0 self.actual_lateral_jerk: float = 0.0
self.lateral_jerk_setpoint: float = 0.0 self.lateral_jerk_setpoint: float = 0.0
@@ -57,6 +53,7 @@ class LatControlTorqueExtBase(LagdToggle):
self.lookahead_lateral_jerk: float = 0.0 self.lookahead_lateral_jerk: float = 0.0
self.torque_from_lateral_accel = lac_torque.torque_from_lateral_accel self.torque_from_lateral_accel = lac_torque.torque_from_lateral_accel
self.torque_params = lac_torque.torque_params
self._ff = 0.0 self._ff = 0.0
self._pid_log = None self._pid_log = None
@@ -8,7 +8,6 @@ See the LICENSE.md file in the root directory for more details.
from cereal import messaging, custom from cereal import messaging, custom
from opendbc.car import structs from opendbc.car import structs
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
from openpilot.sunnypilot.models.helpers import get_active_bundle
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
@@ -16,12 +15,6 @@ DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimen
class LongitudinalPlannerSP: class LongitudinalPlannerSP:
def __init__(self, CP: structs.CarParams, mpc): def __init__(self, CP: structs.CarParams, mpc):
self.dec = DynamicExperimentalController(CP, mpc) self.dec = DynamicExperimentalController(CP, mpc)
self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None
@property
def mlsim(self) -> bool:
# If we don't have a generation set, we assume it's default model. Which as of today are mlsim.
return bool(self.generation is None or self.generation >= 11)
def get_mpc_mode(self) -> str | None: def get_mpc_mode(self) -> str | None:
if not self.dec.active(): if not self.dec.active():
@@ -22,16 +22,14 @@ class ParamStore:
self.keys = universal_params + brand_params self.keys = universal_params + brand_params
self.values = {} self.values = {}
self.cached_params_list: list[capnp.lib.capnp._DynamicStructBuilder] | None = None
def update(self, params: Params) -> None: def update(self, params: Params) -> None:
old_values = dict(self.values)
self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys} self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys}
if old_values != self.values:
self.cached_params_list = None
def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]: def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]:
if self.cached_params_list is None: params_list: list[capnp.lib.capnp._DynamicStructBuilder] = []
# TODO-SP: Why are we doing a list instead of a dictionary here?
self.cached_params_list = [custom.CarControlSP.Param(key=k, value=self.values[k]) for k in self.keys] for k in self.keys:
return self.cached_params_list params_list.append(custom.CarControlSP.Param(key=k, value=self.values[k]))
return params_list
+6 -22
View File
@@ -23,7 +23,6 @@ from typing import cast
from collections.abc import Callable from collections.abc import Callable
import requests import requests
from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK
from jsonrpc import JSONRPCResponseManager, dispatcher from jsonrpc import JSONRPCResponseManager, dispatcher
from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException,
create_connection) create_connection)
@@ -57,11 +56,6 @@ WS_FRAME_SIZE = 4096
DEVICE_STATE_UPDATE_INTERVAL = 1.0 # in seconds DEVICE_STATE_UPDATE_INTERVAL = 1.0 # in seconds
DEFAULT_UPLOAD_PRIORITY = 99 # higher number = lower priority DEFAULT_UPLOAD_PRIORITY = 99 # higher number = lower priority
# https://bytesolutions.com/dscp-tos-cos-precedence-conversion-chart,
# https://en.wikipedia.org/wiki/Differentiated_services
UPLOAD_TOS = 0x20 # CS1, low priority background traffic
SSH_TOS = 0x90 # AF42, DSCP of 36/HDD_LINUX_AC_VI with the minimum delay flag
NetworkType = log.DeviceState.NetworkType NetworkType = log.DeviceState.NetworkType
UploadFileDict = dict[str, str | int | float | bool] UploadFileDict = dict[str, str | int | float | bool]
@@ -70,17 +64,6 @@ UploadItemDict = dict[str, str | bool | int | float | dict[str, str]]
UploadFilesToUrlResponse = dict[str, int | list[UploadItemDict] | list[str]] UploadFilesToUrlResponse = dict[str, int | list[UploadItemDict] | list[str]]
class UploadTOSAdapter(HTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs):
pool_kwargs["socket_options"] = [(socket.IPPROTO_IP, socket.IP_TOS, UPLOAD_TOS)]
super().init_poolmanager(connections, maxsize, block, **pool_kwargs)
UPLOAD_SESS = requests.Session()
UPLOAD_SESS.mount("http://", UploadTOSAdapter())
UPLOAD_SESS.mount("https://", UploadTOSAdapter())
@dataclass @dataclass
class UploadFile: class UploadFile:
fn: str fn: str
@@ -328,10 +311,10 @@ def _do_upload(upload_item: UploadItem, callback: Callable = None) -> requests.R
stream = None stream = None
try: try:
stream, content_length = get_upload_stream(path, compress) stream, content_length = get_upload_stream(path, compress)
response = UPLOAD_SESS.put(upload_item.url, response = requests.put(upload_item.url,
data=CallbackReader(stream, callback, content_length) if callback else stream, data=CallbackReader(stream, callback, content_length) if callback else stream,
headers={**upload_item.headers, 'Content-Length': str(content_length)}, headers={**upload_item.headers, 'Content-Length': str(content_length)},
timeout=30) timeout=30)
return response return response
finally: finally:
if stream: if stream:
@@ -518,7 +501,8 @@ def start_local_proxy_shim(global_end_event: threading.Event, local_port: int, w
raise Exception("Requested local port not whitelisted") raise Exception("Requested local port not whitelisted")
# Set TOS to keep connection responsive while under load. # Set TOS to keep connection responsive while under load.
ws.sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, SSH_TOS) # DSCP of 36/HDD_LINUX_AC_VI with the minimum delay flag
ws.sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 0x90)
ssock, csock = socket.socketpair() ssock, csock = socket.socketpair()
local_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) local_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+3 -3
View File
@@ -19,7 +19,7 @@ from cereal import messaging
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.timeout import Timeout from openpilot.common.timeout import Timeout
from openpilot.system.athena import athenad from openpilot.system.athena import athenad
from openpilot.system.athena.athenad import MAX_RETRY_COUNT, UPLOAD_SESS, dispatcher from openpilot.system.athena.athenad import MAX_RETRY_COUNT, dispatcher
from openpilot.system.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket from openpilot.system.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket
from openpilot.selfdrive.test.helpers import http_server_context from openpilot.selfdrive.test.helpers import http_server_context
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
@@ -29,7 +29,7 @@ def seed_athena_server(host, port):
with Timeout(2, 'HTTP Server seeding failed'): with Timeout(2, 'HTTP Server seeding failed'):
while True: while True:
try: try:
UPLOAD_SESS.put(f'http://{host}:{port}/qlog.zst', data='', timeout=10) requests.put(f'http://{host}:{port}/qlog.zst', data='', timeout=10)
break break
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
time.sleep(0.1) time.sleep(0.1)
@@ -239,7 +239,7 @@ class TestAthenadMethods:
@pytest.mark.parametrize("status,retry", [(500,True), (412,False)]) @pytest.mark.parametrize("status,retry", [(500,True), (412,False)])
@with_upload_handler @with_upload_handler
def test_upload_handler_retry(self, mocker, host, status, retry): def test_upload_handler_retry(self, mocker, host, status, retry):
mock_put = mocker.patch('openpilot.system.athena.athenad.UPLOAD_SESS.put') mock_put = mocker.patch('requests.put')
mock_put.return_value.__enter__.return_value.status_code = status mock_put.return_value.__enter__.return_value.status_code = status
fn = self._create_file('qlog.zst') fn = self._create_file('qlog.zst')
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
-4
View File
@@ -55,8 +55,4 @@ namespace Path {
return "/dev/shm"; return "/dev/shm";
#endif #endif
} }
inline std::string model_root() {
return Hardware::PC() ? Path::comma_home() + "/media/0/models" : "/data/media/0/models";
}
} // namespace Path } // namespace Path
+2 -2
View File
@@ -415,8 +415,8 @@ class Tici(HardwareBase):
# *** GPU config *** # *** GPU config ***
# https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216 # https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") sudo_write("0", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel")
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") sudo_write("0", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel")
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on")
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_clk_on") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_clk_on")
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on")
+22 -55
View File
@@ -62,7 +62,6 @@ struct RemoteEncoder {
bool recording = false; bool recording = false;
bool marked_ready_to_rotate = false; bool marked_ready_to_rotate = false;
bool seen_first_packet = false; bool seen_first_packet = false;
bool audio_initialized = false;
}; };
size_t write_encode_data(LoggerdState *s, cereal::Event::Reader event, RemoteEncoder &re, const EncoderInfo &encoder_info) { size_t write_encode_data(LoggerdState *s, cereal::Event::Reader event, RemoteEncoder &re, const EncoderInfo &encoder_info) {
@@ -79,7 +78,12 @@ size_t write_encode_data(LoggerdState *s, cereal::Event::Reader event, RemoteEnc
LOGW("%s: dropped %d non iframe packets before init", encoder_info.publish_name, re.dropped_frames); LOGW("%s: dropped %d non iframe packets before init", encoder_info.publish_name, re.dropped_frames);
re.dropped_frames = 0; re.dropped_frames = 0;
} }
// if we aren't actually recording, don't create the writer
if (encoder_info.record) { if (encoder_info.record) {
assert(encoder_info.filename != NULL);
re.writer.reset(new VideoWriter(s->logger.segmentPath().c_str(),
encoder_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C,
edata.getWidth(), edata.getHeight(), encoder_info.fps, idx.getType()));
// write the header // write the header
auto header = edata.getHeader(); auto header = edata.getHeader();
re.writer->write((uint8_t *)header.begin(), header.size(), idx.getTimestampEof() / 1000, true, false); re.writer->write((uint8_t *)header.begin(), header.size(), idx.getTimestampEof() / 1000, true, false);
@@ -134,19 +138,12 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct
// if this is a new segment, we close any possible old segments, move to the new, and process any queued packets // if this is a new segment, we close any possible old segments, move to the new, and process any queued packets
if (re.current_segment != s->logger.segment()) { if (re.current_segment != s->logger.segment()) {
// if we aren't actually recording, don't create the writer if (re.recording) {
if (encoder_info.record) { re.writer.reset();
assert(encoder_info.filename != NULL);
re.writer.reset(new VideoWriter(s->logger.segmentPath().c_str(),
encoder_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C,
edata.getWidth(), edata.getHeight(), encoder_info.fps, idx.getType()));
re.recording = false; re.recording = false;
re.audio_initialized = false;
} }
re.current_segment = s->logger.segment(); re.current_segment = s->logger.segment();
re.marked_ready_to_rotate = false; re.marked_ready_to_rotate = false;
}
if (re.audio_initialized || !encoder_info.include_audio) {
// we are in this segment now, process any queued messages before this one // we are in this segment now, process any queued messages before this one
if (!re.q.empty()) { if (!re.q.empty()) {
for (auto qmsg : re.q) { for (auto qmsg : re.q) {
@@ -156,14 +153,9 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct
} }
re.q.clear(); re.q.clear();
} }
bytes_count += write_encode_data(s, event, re, encoder_info);
delete msg;
} else if (re.q.size() > MAIN_FPS*10) {
LOGE_100("%s: dropping frame waiting for audio initialization, queue is too large", name.c_str());
delete msg;
} else {
re.q.push_back(msg); // queue up all the new segment messages, they go in after audio is initialized
} }
bytes_count += write_encode_data(s, event, re, encoder_info);
delete msg;
} else if (offset_segment_num > s->logger.segment()) { } else if (offset_segment_num > s->logger.segment()) {
// encoderd packet has a newer segment, this means encoderd has rolled over // encoderd packet has a newer segment, this means encoderd has rolled over
if (!re.marked_ready_to_rotate) { if (!re.marked_ready_to_rotate) {
@@ -222,7 +214,7 @@ void loggerd_thread() {
typedef struct ServiceState { typedef struct ServiceState {
std::string name; std::string name;
int counter, freq; int counter, freq;
bool encoder, user_flag, record_audio; bool encoder, user_flag;
} ServiceState; } ServiceState;
std::unordered_map<SubSocket*, ServiceState> service_state; std::unordered_map<SubSocket*, ServiceState> service_state;
std::unordered_map<SubSocket*, struct RemoteEncoder> remote_encoders; std::unordered_map<SubSocket*, struct RemoteEncoder> remote_encoders;
@@ -234,22 +226,19 @@ void loggerd_thread() {
for (const auto& [_, it] : services) { for (const auto& [_, it] : services) {
const bool encoder = util::ends_with(it.name, "EncodeData"); const bool encoder = util::ends_with(it.name, "EncodeData");
const bool livestream_encoder = util::starts_with(it.name, "livestream"); const bool livestream_encoder = util::starts_with(it.name, "livestream");
const bool record_audio = (it.name == "rawAudioData") && Params().getBool("RecordAudio"); if (!it.should_log && (!encoder || livestream_encoder)) continue;
if (it.should_log || (encoder && !livestream_encoder) || record_audio) { LOGD("logging %s", it.name.c_str());
LOGD("logging %s", it.name.c_str());
SubSocket * sock = SubSocket::create(ctx.get(), it.name); SubSocket * sock = SubSocket::create(ctx.get(), it.name);
assert(sock != NULL); assert(sock != NULL);
poller->registerSocket(sock); poller->registerSocket(sock);
service_state[sock] = { service_state[sock] = {
.name = it.name, .name = it.name,
.counter = 0, .counter = 0,
.freq = it.decimation, .freq = it.decimation,
.encoder = encoder, .encoder = encoder,
.user_flag = it.name == "userFlag", .user_flag = it.name == "userFlag",
.record_audio = record_audio, };
};
}
} }
LoggerdState s; LoggerdState s;
@@ -258,7 +247,6 @@ void loggerd_thread() {
Params().put("CurrentRoute", s.logger.routeName()); Params().put("CurrentRoute", s.logger.routeName());
std::map<std::string, EncoderInfo> encoder_infos_dict; std::map<std::string, EncoderInfo> encoder_infos_dict;
std::vector<RemoteEncoder*> encoders_with_audio;
for (const auto &cam : cameras_logged) { for (const auto &cam : cameras_logged) {
for (const auto &encoder_info : cam.encoder_infos) { for (const auto &encoder_info : cam.encoder_infos) {
encoder_infos_dict[encoder_info.publish_name] = encoder_info; encoder_infos_dict[encoder_info.publish_name] = encoder_info;
@@ -266,13 +254,6 @@ void loggerd_thread() {
} }
} }
for (auto &[sock, service] : service_state) {
auto it = encoder_infos_dict.find(service.name);
if (it != encoder_infos_dict.end() && it->second.include_audio) {
encoders_with_audio.push_back(&remote_encoders[sock]);
}
}
uint64_t msg_count = 0, bytes_count = 0; uint64_t msg_count = 0, bytes_count = 0;
double start_ts = millis_since_boot(); double start_ts = millis_since_boot();
while (!do_exit) { while (!do_exit) {
@@ -290,20 +271,6 @@ void loggerd_thread() {
Message *msg = nullptr; Message *msg = nullptr;
while (!do_exit && (msg = sock->receive(true))) { while (!do_exit && (msg = sock->receive(true))) {
const bool in_qlog = service.freq != -1 && (service.counter++ % service.freq == 0); const bool in_qlog = service.freq != -1 && (service.counter++ % service.freq == 0);
if (service.record_audio) {
capnp::FlatArrayMessageReader cmsg(kj::ArrayPtr<capnp::word>((capnp::word *)msg->getData(), msg->getSize() / sizeof(capnp::word)));
auto event = cmsg.getRoot<cereal::Event>();
auto audio_data = event.getRawAudioData().getData();
auto sample_rate = event.getRawAudioData().getSampleRate();
for (auto* encoder : encoders_with_audio) {
if (encoder && encoder->writer) {
encoder->writer->write_audio((uint8_t*)audio_data.begin(), audio_data.size(), event.getLogMonoTime() / 1000, sample_rate);
encoder->audio_initialized = true;
}
}
}
if (service.encoder) { if (service.encoder) {
s.last_camera_seen_tms = millis_since_boot(); s.last_camera_seen_tms = millis_since_boot();
bytes_count += handle_encoder_msg(&s, msg, service.name, remote_encoders[sock], encoder_infos_dict[service.name]); bytes_count += handle_encoder_msg(&s, msg, service.name, remote_encoders[sock], encoder_infos_dict[service.name]);
-2
View File
@@ -35,7 +35,6 @@ public:
const char *thumbnail_name = NULL; const char *thumbnail_name = NULL;
const char *filename = NULL; const char *filename = NULL;
bool record = true; bool record = true;
bool include_audio = false;
int frame_width = -1; int frame_width = -1;
int frame_height = -1; int frame_height = -1;
int fps = MAIN_FPS; int fps = MAIN_FPS;
@@ -107,7 +106,6 @@ const EncoderInfo qcam_encoder_info = {
.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264,
.frame_width = 526, .frame_width = 526,
.frame_height = 330, .frame_height = 330,
.include_audio = Params().getBool("RecordAudio"),
INIT_ENCODE_FUNCTIONS(QRoadEncode), INIT_ENCODE_FUNCTIONS(QRoadEncode),
}; };
+35 -76
View File
@@ -97,50 +97,6 @@ class TestLoggerd:
return sent_msgs return sent_msgs
def _publish_camera_and_audio_messages(self, num_segs=1, segment_length=5):
d = DEVICE_CAMERAS[("tici", "ar0231")]
streams = [
(VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048 * 2346, 2048, 2048 * 1216), "roadCameraState"),
(VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048 * 2346, 2048, 2048 * 1216), "driverCameraState"),
(VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048 * 2346, 2048, 2048 * 1216), "wideRoadCameraState"),
]
pm = messaging.PubMaster([s for _, _, s in streams] + ["rawAudioData"])
vipc_server = VisionIpcServer("camerad")
for stream_type, frame_spec, _ in streams:
vipc_server.create_buffers_with_sizes(stream_type, 40, *(frame_spec))
vipc_server.start_listener()
os.environ["LOGGERD_TEST"] = "1"
os.environ["LOGGERD_SEGMENT_LENGTH"] = str(segment_length)
managed_processes["loggerd"].start()
managed_processes["encoderd"].start()
assert pm.wait_for_readers_to_update("roadCameraState", timeout=5)
fps = 20
for n in range(1, int(num_segs * segment_length * fps) + 1):
# send video
for stream_type, frame_spec, state in streams:
dat = np.empty(frame_spec[2], dtype=np.uint8)
vipc_server.send(stream_type, dat[:].flatten().tobytes(), n, n / fps, n / fps)
camera_state = messaging.new_message(state)
frame = getattr(camera_state, state)
frame.frameId = n
pm.send(state, camera_state)
# send audio
msg = messaging.new_message('rawAudioData')
msg.rawAudioData.data = bytes(800 * 2) # 800 samples of int16
msg.rawAudioData.sampleRate = 16000
pm.send('rawAudioData', msg)
for _, _, state in streams:
assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001)
managed_processes["loggerd"].stop()
managed_processes["encoderd"].stop()
def test_init_data_values(self): def test_init_data_values(self):
os.environ["CLEAN"] = random.choice(["0", "1"]) os.environ["CLEAN"] = random.choice(["0", "1"])
@@ -180,23 +136,53 @@ class TestLoggerd:
assert getattr(initData, initData_key) == v assert getattr(initData, initData_key) == v
assert logged_params[param_key].decode() == v assert logged_params[param_key].decode() == v
@pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing @pytest.mark.skip("FIXME: encoderd sometimes crashes in CI when running with pytest-xdist")
def test_rotation(self): def test_rotation(self):
os.environ["LOGGERD_TEST"] = "1"
Params().put("RecordFront", "1") Params().put("RecordFront", "1")
d = DEVICE_CAMERAS[("tici", "ar0231")]
expected_files = {"rlog.zst", "qlog.zst", "qcamera.ts", "fcamera.hevc", "dcamera.hevc", "ecamera.hevc"} expected_files = {"rlog.zst", "qlog.zst", "qcamera.ts", "fcamera.hevc", "dcamera.hevc", "ecamera.hevc"}
streams = [(VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048*2346, 2048, 2048*1216), "roadCameraState"),
(VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048*2346, 2048, 2048*1216), "driverCameraState"),
(VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048*2346, 2048, 2048*1216), "wideRoadCameraState")]
num_segs = random.randint(2, 3) pm = messaging.PubMaster(["roadCameraState", "driverCameraState", "wideRoadCameraState"])
length = random.randint(4, 5) # H264 encoder uses 40 lookahead frames and does B-frame reordering, so minimum 3 seconds before qcam output vipc_server = VisionIpcServer("camerad")
for stream_type, frame_spec, _ in streams:
vipc_server.create_buffers_with_sizes(stream_type, 40, *(frame_spec))
vipc_server.start_listener()
self._publish_camera_and_audio_messages(num_segs=num_segs, segment_length=length) num_segs = random.randint(2, 5)
length = random.randint(1, 3)
os.environ["LOGGERD_SEGMENT_LENGTH"] = str(length)
managed_processes["loggerd"].start()
managed_processes["encoderd"].start()
assert pm.wait_for_readers_to_update("roadCameraState", timeout=5)
fps = 20.0
for n in range(1, int(num_segs*length*fps)+1):
for stream_type, frame_spec, state in streams:
dat = np.empty(frame_spec[2], dtype=np.uint8)
vipc_server.send(stream_type, dat[:].flatten().tobytes(), n, n/fps, n/fps)
camera_state = messaging.new_message(state)
frame = getattr(camera_state, state)
frame.frameId = n
pm.send(state, camera_state)
for _, _, state in streams:
assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001)
managed_processes["loggerd"].stop()
managed_processes["encoderd"].stop()
route_path = str(self._get_latest_log_dir()).rsplit("--", 1)[0] route_path = str(self._get_latest_log_dir()).rsplit("--", 1)[0]
for n in range(num_segs): for n in range(num_segs):
p = Path(f"{route_path}--{n}") p = Path(f"{route_path}--{n}")
logged = {f.name for f in p.iterdir() if f.is_file()} logged = {f.name for f in p.iterdir() if f.is_file()}
diff = logged ^ expected_files diff = logged ^ expected_files
assert len(diff) == 0, f"didn't get all expected files. seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}" assert len(diff) == 0, f"didn't get all expected files. run={_} seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}"
def test_bootlog(self): def test_bootlog(self):
# generate bootlog with fake launch log # generate bootlog with fake launch log
@@ -295,30 +281,3 @@ class TestLoggerd:
segment_dir = self._get_latest_log_dir() segment_dir = self._get_latest_log_dir()
assert getxattr(segment_dir, PRESERVE_ATTR_NAME) is None assert getxattr(segment_dir, PRESERVE_ATTR_NAME) is None
@pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing
@pytest.mark.parametrize("record_front", [True, False])
def test_record_front(self, record_front):
params = Params()
params.put_bool("RecordFront", record_front)
self._publish_camera_and_audio_messages()
dcamera_hevc_exists = os.path.exists(os.path.join(self._get_latest_log_dir(), 'dcamera.hevc'))
assert dcamera_hevc_exists == record_front
@pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing
@pytest.mark.parametrize("record_audio", [True, False])
def test_record_audio(self, record_audio):
params = Params()
params.put_bool("RecordAudio", record_audio)
self._publish_camera_and_audio_messages()
qcamera_ts_path = os.path.join(self._get_latest_log_dir(), 'qcamera.ts')
ffprobe_cmd = f"ffprobe -i {qcamera_ts_path} -show_streams -select_streams a -loglevel error"
has_audio_stream = subprocess.run(ffprobe_cmd, shell=True, capture_output=True).stdout.strip() != b''
assert has_audio_stream == record_audio
raw_audio_in_rlog = any(m.which() == 'rawAudioData' for m in LogReader(os.path.join(self._get_latest_log_dir(), 'rlog.zst')))
assert raw_audio_in_rlog == record_audio
-111
View File
@@ -50,45 +50,6 @@ VideoWriter::VideoWriter(const char *path, const char *filename, bool remuxing,
} }
} }
void VideoWriter::initialize_audio(int sample_rate) {
assert(this->ofmt_ctx->oformat->audio_codec != AV_CODEC_ID_NONE); // check output format supports audio streams
const AVCodec *audio_avcodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
assert(audio_avcodec);
this->audio_codec_ctx = avcodec_alloc_context3(audio_avcodec);
assert(this->audio_codec_ctx);
this->audio_codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
this->audio_codec_ctx->sample_rate = sample_rate;
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1+
av_channel_layout_default(&this->audio_codec_ctx->ch_layout, 1);
#else
this->audio_codec_ctx->channel_layout = AV_CH_LAYOUT_MONO;
#endif
this->audio_codec_ctx->bit_rate = 32000;
this->audio_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
this->audio_codec_ctx->time_base = (AVRational){1, audio_codec_ctx->sample_rate};
int err = avcodec_open2(this->audio_codec_ctx, audio_avcodec, NULL);
assert(err >= 0);
av_log_set_level(AV_LOG_WARNING); // hide "QAvg" info msgs at the end of every segment
this->audio_stream = avformat_new_stream(this->ofmt_ctx, NULL);
assert(this->audio_stream);
err = avcodec_parameters_from_context(this->audio_stream->codecpar, this->audio_codec_ctx);
assert(err >= 0);
this->audio_frame = av_frame_alloc();
assert(this->audio_frame);
this->audio_frame->format = this->audio_codec_ctx->sample_fmt;
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1+
av_channel_layout_copy(&this->audio_frame->ch_layout, &this->audio_codec_ctx->ch_layout);
#else
this->audio_frame->channel_layout = this->audio_codec_ctx->channel_layout;
#endif
this->audio_frame->sample_rate = this->audio_codec_ctx->sample_rate;
this->audio_frame->nb_samples = this->audio_codec_ctx->frame_size;
err = av_frame_get_buffer(this->audio_frame, 0);
assert(err >= 0);
}
void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe) { void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe) {
if (of && data) { if (of && data) {
size_t written = util::safe_fwrite(data, 1, len, of); size_t written = util::safe_fwrite(data, 1, len, of);
@@ -106,10 +67,8 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc
} }
int err = avcodec_parameters_from_context(out_stream->codecpar, codec_ctx); int err = avcodec_parameters_from_context(out_stream->codecpar, codec_ctx);
assert(err >= 0); assert(err >= 0);
// if there is an audio stream, it must be initialized before this point
err = avformat_write_header(ofmt_ctx, NULL); err = avformat_write_header(ofmt_ctx, NULL);
assert(err >= 0); assert(err >= 0);
header_written = true;
} else { } else {
// input timestamps are in microseconds // input timestamps are in microseconds
AVRational in_timebase = {1, 1000000}; AVRational in_timebase = {1, 1000000};
@@ -118,7 +77,6 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc
av_init_packet(&pkt); av_init_packet(&pkt);
pkt.data = data; pkt.data = data;
pkt.size = len; pkt.size = len;
pkt.stream_index = this->out_stream->index;
enum AVRounding rnd = static_cast<enum AVRounding>(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); enum AVRounding rnd = static_cast<enum AVRounding>(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
pkt.pts = pkt.dts = av_rescale_q_rnd(timestamp, in_timebase, ofmt_ctx->streams[0]->time_base, rnd); pkt.pts = pkt.dts = av_rescale_q_rnd(timestamp, in_timebase, ofmt_ctx->streams[0]->time_base, rnd);
@@ -137,80 +95,11 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc
} }
} }
void VideoWriter::write_audio(uint8_t *data, int len, long long timestamp, int sample_rate) {
if (!remuxing) return;
if (!audio_initialized) {
initialize_audio(sample_rate);
audio_initialized = true;
}
if (!audio_codec_ctx) return;
// sync logMonoTime of first audio packet with the timestampEof of first video packet
if (audio_pts == 0) {
audio_pts = (timestamp * audio_codec_ctx->sample_rate) / 1000000ULL;
}
// convert s16le samples to fltp and add to buffer
const int16_t *raw_samples = reinterpret_cast<const int16_t*>(data);
int sample_count = len / sizeof(int16_t);
constexpr float normalizer = 1.0f / 32768.0f;
const size_t max_buffer_size = sample_rate * 10; // 10 seconds
if (audio_buffer.size() + sample_count > max_buffer_size) {
size_t samples_to_drop = (audio_buffer.size() + sample_count) - max_buffer_size;
LOGE("Audio buffer overflow, dropping %zu oldest samples", samples_to_drop);
audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + samples_to_drop);
audio_pts += samples_to_drop;
}
// Add new samples to the buffer
const size_t original_size = audio_buffer.size();
audio_buffer.resize(original_size + sample_count);
std::transform(raw_samples, raw_samples + sample_count, audio_buffer.begin() + original_size,
[](int16_t sample) { return sample * normalizer; });
if (!header_written) return; // header not written yet, process audio frame after header is written
while (audio_buffer.size() >= audio_codec_ctx->frame_size) {
audio_frame->pts = audio_pts;
float *f_samples = reinterpret_cast<float*>(audio_frame->data[0]);
std::copy(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size, f_samples);
audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size);
encode_and_write_audio_frame(audio_frame);
}
}
void VideoWriter::encode_and_write_audio_frame(AVFrame* frame) {
if (!remuxing || !audio_codec_ctx) return;
int send_result = avcodec_send_frame(audio_codec_ctx, frame); // encode frame
if (send_result >= 0) {
AVPacket *pkt = av_packet_alloc();
while (avcodec_receive_packet(audio_codec_ctx, pkt) == 0) {
av_packet_rescale_ts(pkt, audio_codec_ctx->time_base, audio_stream->time_base);
pkt->stream_index = audio_stream->index;
int err = av_interleaved_write_frame(ofmt_ctx, pkt); // write encoded frame
if (err < 0) {
LOGW("AUDIO: Write frame failed - error: %d", err);
}
av_packet_unref(pkt);
}
av_packet_free(&pkt);
} else {
LOGW("AUDIO: Failed to send audio frame to encoder: %d", send_result);
}
audio_pts += audio_codec_ctx->frame_size;
}
VideoWriter::~VideoWriter() { VideoWriter::~VideoWriter() {
if (this->remuxing) { if (this->remuxing) {
if (this->audio_codec_ctx) {
encode_and_write_audio_frame(NULL); // flush encoder
avcodec_free_context(&this->audio_codec_ctx);
}
int err = av_write_trailer(this->ofmt_ctx); int err = av_write_trailer(this->ofmt_ctx);
if (err != 0) LOGE("av_write_trailer failed %d", err); if (err != 0) LOGE("av_write_trailer failed %d", err);
avcodec_free_context(&this->codec_ctx); avcodec_free_context(&this->codec_ctx);
if (this->audio_frame) av_frame_free(&this->audio_frame);
err = avio_closep(&this->ofmt_ctx->pb); err = avio_closep(&this->ofmt_ctx->pb);
if (err != 0) LOGE("avio_closep failed %d", err); if (err != 0) LOGE("avio_closep failed %d", err);
avformat_free_context(this->ofmt_ctx); avformat_free_context(this->ofmt_ctx);
-16
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <string> #include <string>
#include <deque>
extern "C" { extern "C" {
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
@@ -14,28 +13,13 @@ class VideoWriter {
public: public:
VideoWriter(const char *path, const char *filename, bool remuxing, int width, int height, int fps, cereal::EncodeIndex::Type codec); VideoWriter(const char *path, const char *filename, bool remuxing, int width, int height, int fps, cereal::EncodeIndex::Type codec);
void write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe); void write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe);
void write_audio(uint8_t *data, int len, long long timestamp, int sample_rate);
~VideoWriter(); ~VideoWriter();
private: private:
void initialize_audio(int sample_rate);
void encode_and_write_audio_frame(AVFrame* frame);
std::string vid_path, lock_path; std::string vid_path, lock_path;
FILE *of = nullptr; FILE *of = nullptr;
AVCodecContext *codec_ctx; AVCodecContext *codec_ctx;
AVFormatContext *ofmt_ctx; AVFormatContext *ofmt_ctx;
AVStream *out_stream; AVStream *out_stream;
bool audio_initialized = false;
bool header_written = false;
AVStream *audio_stream = nullptr;
AVCodecContext *audio_codec_ctx = nullptr;
AVFrame *audio_frame = nullptr;
uint64_t audio_pts = 0;
std::deque<float> audio_buffer;
bool remuxing; bool remuxing;
}; };

Some files were not shown because too many files have changed in this diff Show More