Compare commits

..

4 Commits

Author SHA1 Message Date
DevTekVE
258205e9f7 conflic solve 2025-06-26 19:49:03 +02:00
DevTekVE
fe46463c66 Merge remote-tracking branch 'origin/master-new' into joystick-xbox-one
# Conflicts:
#	uv.lock
2025-06-26 19:48:05 +02:00
DevTekVE
888c9e9620 fixes on joystick 2025-04-18 19:37:12 +02:00
DevTekVE
baac950325 Handle missing pygame import gracefully
Wrap the pygame import in a try-except block to catch ImportError. This prevents the script from crashing and provides a clear message prompting the user to install pygame if it's missing.

Remove "inputs" package and update "pygame" dependency

The "inputs" package has been removed from the lockfile and dependency list, while "pygame" is now included universally without the "dev" extra marker. This change simplifies dependencies and ensures consistency across environments.

Update dependencies: replace 'inputs' with 'pygame'

Replaced the 'inputs' library with 'pygame' for joystickd dependencies in `pyproject.toml`. Additionally, removed a redundant 'pygame' entry from the general dependencies.

Ugly, I know, but soundd is unhappy with joystick

Allowing lat with mads

Invert steering input for joystick control

The steering axis input is now multiplied by -1 to reverse its direction. This ensures correct handling of the left stick's horizontal input, aligning behavior with expected control dynamics.

Refactor joystick control to use pygame for broader support

Replaced the `inputs` library with `pygame` for joystick handling, providing improved compatibility with Xbox and PlayStation controllers. Added initialization, adaptive mappings, deadzone handling, and enhanced event processing for robust joystick operation. Updated README with dependencies and usage information for Xbox controllers.
2025-04-18 19:37:10 +02:00
206 changed files with 4106 additions and 9969 deletions

View File

@@ -2,5 +2,3 @@ Wen
REGIST
PullRequest
cancelled
indeces
FOF

View File

@@ -1,226 +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 }}
steps:
- 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: Get next JSON version to use
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=$json_file" >> $GITHUB_OUTPUT
- name: Upload context for next jobs
uses: actions/upload-artifact@v4
with:
name: context
path: docs
build-all:
runs-on: ubuntu-latest
needs: setup
env:
JSON_FILE: docs/docs/${{ needs.setup.outputs.json_file }}
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
git checkout main
cd ..
- name: Set next recompiled dir
id: set-recompiled
run: |
cd gitlab_docs/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}"
mkdir -p "$recompiled_dir"
echo "RECOMPILED_DIR=$recompiled_dir" >> $GITHUB_ENV
- name: Download context
uses: actions/download-artifact@v4
with:
name: context
path: .
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq gh
- name: Build all tinygrad models
id: trigger-builds
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
> triggered_run_ids.txt
BRANCH="${{ github.event.inputs.branch }}"
jq -c '.bundles[] | select(.runner=="tinygrad")' "$JSON_FILE" | while read -r bundle; do
ref=$(echo "$bundle" | jq -r '.ref')
display_name=$(echo "$bundle" | jq -r '.display_name' | sed 's/ ([^)]*)//g')
is_20hz=$(echo "$bundle" | jq -r '.is_20hz')
echo "Triggering build for: $display_name ($ref) [20Hz: $is_20hz]"
START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
gh workflow run sunnypilot-build-model.yaml \
--repo sunnypilot/sunnypilot \
--ref "$BRANCH" \
-f upstream_branch="$ref" \
-f custom_name="$display_name" \
-f is_20hz="$is_20hz"
for i in {1..24}; do
RUN_ID=$(gh run list --repo sunnypilot/sunnypilot --workflow=sunnypilot-build-model.yaml --branch="$BRANCH" --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 "ould not find the triggered workflow run for $display_name ($ref)"
exit 1
fi
echo "$RUN_ID" >> triggered_run_ids.txt
done
- name: Wait for all model builds to finish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
SUCCESS_RUNS=()
FAILED_RUNS=()
declare -A RUN_ID_TO_NAME
while read -r RUN_ID; do
ARTIFACT_NAME=$(gh api repos/sunnypilot/sunnypilot/actions/runs/$RUN_ID/artifacts --jq '.artifacts[] | select(.name | startswith("model-")) | .name' || echo "unknown")
RUN_ID_TO_NAME["$RUN_ID"]="$ARTIFACT_NAME"
done < triggered_run_ids.txt
while read -r RUN_ID; do
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')
ARTIFACT_NAME="${RUN_ID_TO_NAME[$RUN_ID]}"
echo "Run $RUN_ID ($ARTIFACT_NAME) concluded with: $CONCLUSION"
if [[ "$CONCLUSION" == "success" ]]; then
SUCCESS_RUNS+=("$RUN_ID")
else
FAILED_RUNS+=("$RUN_ID")
fi
done < triggered_run_ids.txt
if [[ ${#SUCCESS_RUNS[@]} -eq 0 ]]; then
echo "All model builds failed. Aborting."
exit 1
fi
if [[ ${#FAILED_RUNS[@]} -gt 0 ]]; then
echo "WARNING: The following model builds failed:"
for RUN_ID in "${FAILED_RUNS[@]}"; do
echo "- $RUN_ID (${RUN_ID_TO_NAME[$RUN_ID]})"
done
echo "You may want to rerun these models manually."
fi
- name: Download and extract all model artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ARTIFACT_DIR="gitlab_docs/models/$RECOMPILED_DIR"
SUCCESS_RUNS=()
while read -r RUN_ID; do
CONCLUSION=$(gh run view "$RUN_ID" --repo sunnypilot/sunnypilot --json conclusion --jq '.conclusion')
if [[ "$CONCLUSION" == "success" ]]; then
SUCCESS_RUNS+=("$RUN_ID")
fi
done < triggered_run_ids.txt
for RUN_ID in "${SUCCESS_RUNS[@]}"; do
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 (This is NOT an error)."
fi
echo "Done processing $ARTIFACT_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 "$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 $JSON_FILE)"
git commit -m "Update $(basename $JSON_FILE) after recompiling models" || echo "No changes to commit"
git push origin gh-pages

View File

@@ -1,198 +0,0 @@
name: Build Single Tinygrad Model and Push
on:
workflow_dispatch:
inputs:
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: ubuntu-latest
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

View File

@@ -54,9 +54,8 @@ jobs:
git add .
- name: update car docs
run: |
export PYTHONPATH="$PWD"
scons -j$(nproc) --minimal opendbc_repo
python selfdrive/car/docs.py
PYTHONPATH=. python selfdrive/car/docs.py
git add docs/CARS.md
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83

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
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:
build_release:
@@ -163,6 +163,12 @@ jobs:
./selfdrive/ui/tests/create_test_translations.sh && \
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \
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:
name: process replay
@@ -186,8 +192,10 @@ jobs:
- name: Run replay
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && 1 || 20 }}
run: |
${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache"
${{ env.RUN }} "coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache && \
coverage combine && \
coverage xml"
- name: Print diff
id: print-diff
if: always()
@@ -199,7 +207,7 @@ jobs:
name: process_replay_diff.txt
path: selfdrive/test/process_replay/diff.txt
- 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: |
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- name: Run regen
@@ -208,6 +216,12 @@ jobs:
run: |
${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \
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:
name: cars
@@ -238,6 +252,12 @@ jobs:
env:
NUM_JOBS: 4
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:
name: PR comments

View File

@@ -7,7 +7,6 @@ on:
env:
DAYS_BEFORE_PR_CLOSE: 2
DAYS_BEFORE_PR_STALE: 9
DAYS_BEFORE_PR_STALE_DRAFT: 30
jobs:
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
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
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
days-before-issue-stale: -1 # ignore issues for now

2
.gitignore vendored
View File

@@ -70,6 +70,8 @@ flycheck_*
cppcheck_report.txt
comma*.sh
selfdrive/modeld/thneed/compile
selfdrive/modeld/models/*.thneed
selfdrive/modeld/models/*.pkl
sunnypilot/modeld*/thneed/compile
sunnypilot/modeld*/models/*.thneed

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">
<exec>
<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$" />
</exec>
</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">
<exec>
<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$" />
</exec>
</tool>

View File

@@ -3,6 +3,7 @@ FROM ghcr.io/commaai/openpilot-base:latest
ENV PYTHONUNBUFFERED=1
ENV OPENPILOT_PATH=/home/batman/openpilot
ENV PYTHONPATH=${OPENPILOT_PATH}:${PYTHONPATH}
RUN mkdir -p ${OPENPILOT_PATH}
WORKDIR ${OPENPILOT_PATH}

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

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)
========================

View File

@@ -39,6 +39,10 @@ AddOption('--clazy',
action='store_true',
help='build with clazy')
AddOption('--compile_db',
action='store_true',
help='build clang compilation database')
AddOption('--ccflags',
action='store',
type='string',
@@ -230,7 +234,8 @@ if arch == "Darwin":
darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]]
env["LINKFLAGS"] += darwin_rpath_link_flags
env.CompilationDatabase('compile_commands.json')
if GetOption('compile_db'):
env.CompilationDatabase('compile_commands.json')
# Setup cache dir
default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache'

View File

@@ -1083,7 +1083,7 @@ struct ModelDataV2 {
confidence @23: ConfidenceClass;
# Model perceived motion
temporalPoseDEPRECATED @21 :Pose;
temporalPose @21 :Pose;
# e2e lateral planner
action @26: Action;
@@ -2281,7 +2281,6 @@ struct LiveTorqueParametersData {
points @10 :List(List(Float32));
version @11 :Int32;
useParams @12 :Bool;
calPerc @13 :Int8;
}
struct LiveDelayData {
@@ -2292,7 +2291,6 @@ struct LiveDelayData {
lateralDelayEstimate @3 :Float32;
lateralDelayEstimateStd @5 :Float32;
points @4 :List(Float32);
calPerc @6 :Int8;
enum Status {
unestimated @0;
@@ -2470,19 +2468,13 @@ struct DebugAlert {
struct UserFlag {
}
struct SoundPressure @0xdc24138990726023 {
struct Microphone {
soundPressure @0 :Float32;
# uncalibrated, A-weighted
soundPressureWeighted @3 :Float32;
soundPressureWeightedDb @1 :Float32;
filteredSoundPressureWeightedDbDEPRECATED @2 :Float32;
}
struct AudioData {
data @0 :Data;
sampleRate @1 :UInt32;
filteredSoundPressureWeightedDb @2 :Float32;
}
struct Touch {
@@ -2562,8 +2554,7 @@ struct Event {
livestreamDriverEncodeIdx @119 :EncodeIndex;
# microphone data
soundPressure @103 :SoundPressure;
rawAudioData @147 :AudioData;
microphone @103 :Microphone;
# systems stuff
androidLog @20 :AndroidLogEntry;

View File

@@ -73,8 +73,7 @@ _services: dict[str, tuple] = {
"navThumbnail": (True, 0.),
"qRoadEncodeIdx": (False, 20.),
"userFlag": (True, 0., 1),
"soundPressure": (True, 10., 10),
"rawAudioData": (False, 20.),
"microphone": (True, 10., 10),
# sunnypilot
"modelManagerSP": (False, 1., 1),

13
codecov.yml Normal file
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"

View File

@@ -1 +1 @@
#define DEFAULT_MODEL "Tomb Raider 14 (Default)"
#define DEFAULT_MODEL "Filet o Fish (Default)"

View File

@@ -5,8 +5,8 @@
inline static std::unordered_map<std::string, uint32_t> keys = {
{"AccessToken", CLEAR_ON_MANAGER_START | DONT_LOG},
{"AdbEnabled", PERSISTENT | BACKUP},
{"AlwaysOnDM", PERSISTENT | BACKUP},
{"AdbEnabled", PERSISTENT},
{"AlwaysOnDM", PERSISTENT},
{"ApiCache_Device", PERSISTENT},
{"ApiCache_FirehoseStats", PERSISTENT},
{"AssistNowToken", PERSISTENT},
@@ -78,7 +78,7 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"LocationFilterInitialState", PERSISTENT},
{"LongitudinalManeuverMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"LongitudinalPersonality", PERSISTENT | BACKUP},
{"NetworkMetered", PERSISTENT | BACKUP},
{"NetworkMetered", PERSISTENT},
{"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"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},
{"PandaSignatures", CLEAR_ON_MANAGER_START},
{"PrimeType", PERSISTENT},
{"RecordAudio", PERSISTENT | BACKUP},
{"RecordFront", PERSISTENT | BACKUP},
{"RecordFrontLock", PERSISTENT}, // for the internal fleet
{"SecOCKey", PERSISTENT | DONT_LOG | BACKUP},
{"SecOCKey", PERSISTENT | DONT_LOG}, // Candidate for | BACKUP
{"RouteCount", PERSISTENT},
{"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"SshEnabled", PERSISTENT | BACKUP},
@@ -124,29 +123,24 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// --- sunnypilot params --- //
{"ApiCache_DriveStats", PERSISTENT},
{"AutoLaneChangeBsmDelay", PERSISTENT | BACKUP},
{"AutoLaneChangeTimer", PERSISTENT | BACKUP},
{"AutoLaneChangeBsmDelay", PERSISTENT},
{"AutoLaneChangeTimer", PERSISTENT},
{"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP},
{"BlinkerPauseLateralControl", PERSISTENT | BACKUP},
{"Brightness", PERSISTENT | BACKUP},
{"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"CarParamsSPCache", CLEAR_ON_MANAGER_START},
{"CarParamsSPPersistent", PERSISTENT},
{"CarPlatformBundle", PERSISTENT | BACKUP},
{"ChevronInfo", PERSISTENT | BACKUP},
{"CarPlatformBundle", PERSISTENT},
{"CustomAccIncrementsEnabled", PERSISTENT | BACKUP},
{"CustomAccLongPressIncrement", PERSISTENT | BACKUP},
{"CustomAccShortPressIncrement", PERSISTENT | BACKUP},
{"DeviceBootMode", PERSISTENT | BACKUP},
{"EnableGithubRunner", PERSISTENT | BACKUP},
{"InteractivityTimeout", PERSISTENT | BACKUP},
{"IsDevelopmentBranch", CLEAR_ON_MANAGER_START},
{"MaxTimeOffroad", PERSISTENT | BACKUP},
{"Brightness", PERSISTENT | BACKUP},
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
{"OffroadMode", CLEAR_ON_MANAGER_START},
{"QuickBootToggle", PERSISTENT | BACKUP},
{"QuietMode", PERSISTENT | BACKUP},
{"ShowAdvancedControls", PERSISTENT | BACKUP},
// MADS params
{"Mads", PERSISTENT | BACKUP},
@@ -156,7 +150,6 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// Model Manager params
{"ModelManager_ActiveBundle", PERSISTENT},
{"ModelManager_ClearCache", CLEAR_ON_MANAGER_START},
{"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"ModelManager_ModelsCache", PERSISTENT | BACKUP},
@@ -178,15 +171,13 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"BackupManager_RestoreVersion", PERSISTENT},
// sunnypilot car specific params
{"HyundaiLongitudinalTuning", PERSISTENT | BACKUP},
{"HyundaiLongitudinalTuning", PERSISTENT},
{"DynamicExperimentalControl", PERSISTENT | BACKUP},
{"DynamicExperimentalControl", PERSISTENT},
{"BlindSpot", PERSISTENT | BACKUP},
// model panel params
{"LagdToggle", PERSISTENT | BACKUP},
{"LagdToggleDesc", PERSISTENT},
{"LagdToggledelay", PERSISTENT | BACKUP},
// mapd
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
@@ -207,13 +198,4 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"OsmStateTitle", PERSISTENT},
{"OsmWayTest", PERSISTENT},
{"RoadName", CLEAR_ON_ONROAD_TRANSITION},
// Tuning keys
{"EnableHkgTuningAngleSmoothingFactor", PERSISTENT | BACKUP},
{"HkgTuningAngleMinTorqueReductionGain", PERSISTENT | BACKUP},
{"HkgTuningAngleMaxTorqueReductionGain", PERSISTENT | BACKUP},
{"HkgTuningAngleActiveTorqueReductionGain", PERSISTENT | BACKUP},
{"HkgTuningOverridingCycles", PERSISTENT | BACKUP},
{"HkgAngleLiveTuning", CLEAR_ON_MANAGER_START}
};

View File

@@ -1,52 +0,0 @@
import os
import subprocess
from openpilot.common.basedir import BASEDIR
class Spinner:
def __init__(self):
try:
self.spinner_proc = subprocess.Popen(["./spinner.py"],
stdin=subprocess.PIPE,
cwd=os.path.join(BASEDIR, "system", "ui"),
close_fds=True)
except OSError:
self.spinner_proc = None
def __enter__(self):
return self
def update(self, spinner_text: str):
if self.spinner_proc is not None:
self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n")
try:
self.spinner_proc.stdin.flush()
except BrokenPipeError:
pass
def update_progress(self, cur: float, total: float):
self.update(str(round(100 * cur / total)))
def close(self):
if self.spinner_proc is not None:
self.spinner_proc.kill()
try:
self.spinner_proc.communicate(timeout=2.)
except subprocess.TimeoutExpired:
print("WARNING: failed to kill spinner")
self.spinner_proc = None
def __del__(self):
self.close()
def __exit__(self, exc_type, exc_value, traceback):
self.close()
if __name__ == "__main__":
import time
with Spinner() as s:
s.update("Spinner text")
time.sleep(5.0)
print("gone")
time.sleep(5.0)

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env python3
import os
import time
import subprocess
from openpilot.common.basedir import BASEDIR
class TextWindow:
def __init__(self, text):
try:
self.text_proc = subprocess.Popen(["./text.py", text],
stdin=subprocess.PIPE,
cwd=os.path.join(BASEDIR, "system", "ui"),
close_fds=True)
except OSError:
self.text_proc = None
def get_status(self):
if self.text_proc is not None:
self.text_proc.poll()
return self.text_proc.returncode
return None
def __enter__(self):
return self
def close(self):
if self.text_proc is not None:
self.text_proc.terminate()
self.text_proc = None
def wait_for_exit(self):
if self.text_proc is not None:
while True:
if self.get_status() == 1:
return
time.sleep(0.1)
def __del__(self):
self.close()
def __exit__(self, exc_type, exc_value, traceback):
self.close()
if __name__ == "__main__":
text = """Traceback (most recent call last):
File "./controlsd.py", line 608, in <module>
main()
File "./controlsd.py", line 604, in main
controlsd_thread(sm, pm, logcan)
File "./controlsd.py", line 455, in controlsd_thread
1/0
ZeroDivisionError: division by zero"""
print(text)
with TextWindow(text) as s:
for _ in range(100):
if s.get_status() == 1:
print("Got exit button")
break
time.sleep(0.1)
print("gone")

View File

@@ -1 +1 @@
#define COMMA_VERSION "0.10.0"
#define COMMA_VERSION "0.9.10"

View File

@@ -2,6 +2,7 @@ import contextlib
import gc
import os
import pytest
import random
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.system.manager import manager
@@ -48,6 +49,8 @@ def clean_env():
@pytest.fixture(scope="function", autouse=True)
def openpilot_function_fixture(request):
random.seed(0)
with clean_env():
# 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:

View File

@@ -15,7 +15,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 2014-19">Buy Here</a></sub></details>|||
|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q2 2018">Buy Here</a></sub></details>|||
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q3 2019-24">Buy Here</a></sub></details>|||
|Audi|Q3 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q3 2019-23">Buy Here</a></sub></details>|||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=RS3 2018">Buy Here</a></sub></details>|||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=S3 2015-17">Buy Here</a></sub></details>|||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<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=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
@@ -38,7 +38,6 @@ A supported vehicle is one that just works when you install a comma device. All
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 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=Ford&model=Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Expedition 2022-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 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=Ford&model=Explorer 2020-24">Buy Here</a></sub></details>|||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 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=Ford&model=Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=F-150 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
@@ -54,7 +53,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2023-24">Buy Here</a></sub></details>|||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Mustang Mach-E 2021-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|Ford|Mustang Mach-E 2021-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Mustang Mach-E 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Ranger 2024">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F 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=Genesis&model=G70 2018">Buy Here</a></sub></details>|||
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F 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=Genesis&model=G70 2019-21">Buy Here</a></sub></details>|||
@@ -78,8 +77,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B 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=Honda&model=Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda&model=Civic Hatchback 2017-21">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B 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=Honda&model=Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Honda|Civic Hatchback Hybrid 2023 (Europe only)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B 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=Honda&model=Civic Hatchback Hybrid 2023 (Europe only)">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B 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=Honda&model=Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B 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=Honda&model=Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec 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=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>|||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>|||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda&model=CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
@@ -116,6 +115,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C 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=Hyundai&model=Ioniq Plug-in Hybrid 2019">Buy Here</a></sub></details>|||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H 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=Hyundai&model=Ioniq Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B 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=Hyundai&model=Kona 2020">Buy Here</a></sub></details>|||
|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O 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=Hyundai&model=Kona 2022">Buy Here</a></sub></details>|||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G 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=Hyundai&model=Kona Electric 2018-21">Buy Here</a></sub></details>|||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O 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=Hyundai&model=Kona Electric 2022-23">Buy Here</a></sub></details>|||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R 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=Hyundai&model=Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
@@ -181,13 +181,13 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H 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=Kia&model=Telluride 2020-22">Buy Here</a></sub></details>|||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|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=CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|Lexus|ES 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|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=ES 2017-18">Buy Here</a></sub></details>|||
|Lexus|ES 2019-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=ES 2019-25">Buy Here</a></sub></details>|||
|Lexus|ES 2019-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=ES 2019-24">Buy Here</a></sub></details>|||
|Lexus|ES Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|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=ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|Lexus|ES Hybrid 2019-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=ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|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 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|IS 2022-23|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-23">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 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>|||
@@ -313,11 +313,11 @@ A supported vehicle is one that just works when you install a comma device. All
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Roc 2018-23">Buy Here</a></sub></details>|||
|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Taos 2022-24">Buy Here</a></sub></details>|||
|Volkswagen|Taos 2022-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Taos 2022-23">Buy Here</a></sub></details>|||
|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont 2018-22">Buy Here</a></sub></details>|||
|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont X 2021-22">Buy Here</a></sub></details>|||
|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan 2018-24">Buy Here</a></sub></details>|||
|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan 2018-23">Buy Here</a></sub></details>|||
|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Touran 2016-23">Buy Here</a></sub></details>|||

View File

@@ -7,6 +7,7 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu
### Getting Started
* Setup your [development environment](../tools/)
* Read about the [development workflow](WORKFLOW.md)
* Join our [Discord](https://discord.comma.ai)
* Docs are at https://docs.comma.ai and https://blog.comma.ai

33
docs/WORKFLOW.md Normal file
View File

@@ -0,0 +1,33 @@
# openpilot development workflow
Aside from the ML models, most tools used for openpilot development are in this repo.
Most development happens on normal Ubuntu workstations, and not in cars or directly on comma devices. See the [setup guide](../tools) for getting your PC setup for openpilot development.
## Quick start
```bash
# get the latest stuff
git pull
git lfs pull
git submodule update --init --recursive
# update dependencies
tools/ubuntu_setup.sh
# build everything
scons -j$(nproc)
# build just the ui with either of these
scons -j8 selfdrive/ui/
cd selfdrive/ui/ && scons -u -j8
# test everything
pytest
# test just logging services
cd system/loggerd && pytest .
# run the linter
op lint
```

View File

@@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="12.4"
export AGNOS_VERSION="12.3"
fi
export STAGING_ROOT="/data/safe_staging"

2
panda

Submodule panda updated: c33cfa0803...5ac4fa5bb0

View File

@@ -68,9 +68,6 @@ dependencies = [
# logreader
"zstandard",
# ui
"qrcode",
]
[project.optional-dependencies]
@@ -81,13 +78,14 @@ docs = [
]
testing = [
"coverage",
"hypothesis ==6.47.*",
"mypy",
"pytest",
"pytest-cov",
"pytest-cpp",
"pytest-subtests",
# https://github.com/pytest-dev/pytest-xdist/issues/1215
"pytest-xdist @ git+https://github.com/sshane/pytest-xdist@909e97b49d12401c10608f9d777bfc9dab8a4413",
"pytest-xdist",
"pytest-timeout",
"pytest-randomly",
"pytest-asyncio",
@@ -104,8 +102,8 @@ dev = [
"azure-storage-blob",
"dbus-next",
"dictdiffer",
"lru-dict",
"matplotlib",
"opencv-python-headless",
"parameterized >=0.8, <0.9",
"pyautogui",
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
@@ -263,5 +261,8 @@ lint.flake8-implicit-str-concat.allow-multiline = false
"unittest".msg = "Use pytest"
"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure"
[tool.coverage.run]
concurrency = ["multiprocessing", "thread"]
[tool.ruff.format]
quote-style = "preserve"

View File

@@ -1,31 +1,36 @@
# openpilot releases
```
## release checklist
**Go to `devel-staging`**
- [ ] update RELEASES.md
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
- [ ] open a pull request from `devel-staging` to `devel`
- [ ] post on Discord
**Go to `devel`**
- [ ] update RELEASES.md
- [ ] close out milestone
- [ ] post on Discord dev channel
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
- [ ] before merging the pull request
- [ ] update from previous release -> new release
- [ ] update from new release -> previous release
- [ ] fresh install with `openpilot-test.comma.ai`
- [ ] drive on fresh install
- [ ] no submodules or LFS
- [ ] check sentry, MTBF, etc.
- [ ] merge the pull request
tests:
- [ ] update from previous release -> new release
- [ ] update from new release -> previous release
- [ ] fresh install with `openpilot-test.comma.ai`
- [ ] drive on fresh install
- [ ] comma body test
- [ ] no submodules or LFS
- [ ] check sentry, MTBF, etc.
**Go to `release3`**
- [ ] publish the blog post
- [ ] `git reset --hard origin/release3-staging`
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
- [ ] tag the release
```
git tag v0.X.X <commit-hash>
git push origin v0.X.X
```
- [ ] create GitHub release
- [ ] final test install on `openpilot.comma.ai`
- [ ] update factory provisioning
- [ ] close out milestone
- [ ] post on Discord, X, etc.
```
- [ ] update production
- [ ] Post on Discord, X, etc.

View File

@@ -5,7 +5,6 @@ set -e
DEFAULT_REPO_URL="https://github.com/sunnypilot"
START_AT_BOOT=false
RESTORE_MODE=false
RUNNER_VERSION="2.325.0"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
@@ -109,9 +108,9 @@ setup_system_configs() {
install_runner() {
echo "Downloading and setting up runner..."
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
sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
sudo rm ./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-2.322.0.tar.gz
sudo rm ./actions-runner-linux-arm64-2.322.0.tar.gz
sudo chmod +x ./config.sh
}
@@ -150,32 +149,25 @@ EOL
}
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
local service_path="/etc/systemd/system/${service_name}"
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"
sudo ./svc.sh install $RUNNER_USER
if [ "$START_AT_BOOT" = false ]; then
local service_name
if [ -f "${RUNNER_DIR}/.service" ]; then
service_name=$(cat "${RUNNER_DIR}/.service")
else
service_name="actions.runner.sunnypilot.$(uname -n)"
fi
sudo systemctl disable "${service_name}"
fi
remount_ro
}
check_restore_prerequisites() {
local needs_restore=false
local can_restore=false
local service_name=""
@@ -197,16 +189,25 @@ check_restore_prerequisites() {
exit 1
fi
# Then check if restoration is needed (if either service or user is missing)
if ! systemctl list-unit-files "${service_name}" &>/dev/null; then
echo "Service ${service_name} not found in systemd"
needs_restore=true
fi
if ! id "${RUNNER_USER}" &>/dev/null; then
echo "User ${RUNNER_USER} does not exist"
needs_restore=true
fi
# Only proceed if we can restore AND need to restore
if [ "$can_restore" = true ]; then
echo "Restoration is possible"
if [ "$can_restore" = true ] && [ "$needs_restore" = true ]; then
echo "Restoration is needed and possible"
return 0
else
echo "No restoration possible"
if [ "$needs_restore" = false ]; then
echo "System is already properly configured (user and service exist)"
fi
exit 0
fi
}
@@ -225,6 +226,7 @@ perform_install() {
setup_system_configs
install_runner
set_directory_permissions
create_service_template
configure_runner
install_service
echo "Installation completed successfully"

View File

@@ -13,7 +13,7 @@ cd $ROOT
FAILED=0
IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md|layouts\/.*\.xml"
IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md"
IGNORED_DIRS="^third_party.*|^msgq.*|^msgq_repo.*|^opendbc.*|^opendbc_repo.*|^cereal.*|^panda.*|^rednose.*|^rednose_repo.*|^tinygrad.*|^tinygrad_repo.*|^teleoprtc.*|^teleoprtc_repo.*"
function run() {

Binary file not shown.

View File

@@ -12,10 +12,11 @@
<h5>Quectel/EG25-G</h5>
<p>FCC ID: XMR201903EG25G</p>
<p>This device complies with Part 15 of the FCC Rules.</p>
<p>Operation is subject to the following two conditions:</p>
<p>
This device complies with Part 15 of the FCC Rules.
Operation is subject to the following two conditions:
<p>(1) this device may not cause harmful interference, and</p>
<p>(1) this device may not cause harmful interference, and
<p>(2) this device must accept any interference received, including interference that may cause undesired operation.</p>
The following test reports are subject to this declaration:

View File

@@ -17,7 +17,6 @@ from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
from openpilot.selfdrive.controls.lib.latcontrol_angle_torque import LatControlAngleTorque
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
@@ -59,9 +58,7 @@ class Controls(ControlsExt):
self.LoC = LongControl(self.CP)
self.VM = VehicleModel(self.CP)
self.LaC: LatControl
if self.CP.steerControlType == car.CarParams.SteerControlType.angle and self.CP.lateralTuning.which() == 'torque':
self.LaC = LatControlAngleTorque(self.CP, self.CP_SP, self.CI)
elif self.CP.steerControlType == car.CarParams.SteerControlType.angle:
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI)
elif self.CP.lateralTuning.which() == 'pid':
self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI)
@@ -96,8 +93,7 @@ class Controls(ControlsExt):
torque_params.frictionCoefficientFiltered)
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(calculated_lag)
self.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay)
long_plan = self.sm['longitudinalPlan']
model_v2 = self.sm['modelV2']

View File

@@ -1,4 +1,5 @@
import numpy as np
from cereal import log
from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
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
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):
if len(speeds) == len(t_idxs):
v_now = speeds[0]

View File

@@ -1,13 +0,0 @@
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
class LatControlAngleTorque(LatControlTorque, LatControlAngle):
def __init__(self, CP, CP_SP, CI):
LatControlTorque.__init__(self, CP, CP_SP, CI)
LatControlAngle.__init__(self, CP, CP_SP, CI)
def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited):
torque, _, _ = LatControlTorque.update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited)
_, angle, angle_log = LatControlAngle.update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited)
return torque, angle, angle_log

View File

@@ -31,6 +31,7 @@ class LatControlTorque(LatControl):
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)
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.extension = LatControlTorqueExt(self, CP, CP_SP)
@@ -46,9 +47,16 @@ class LatControlTorque(LatControl):
output_torque = 0.0
pid_log.active = False
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
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 rate is the desired rate of change in the setpoint, not the absolute desired curvature

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.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.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.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):
self.CP = CP
self.mpc = LongitudinalMpc(dt=dt)
# TODO remove mpc modes when TR released
self.mpc.mode = 'acc'
LongitudinalPlannerSP.__init__(self, self.CP, self.mpc)
self.fcw = False
@@ -64,6 +63,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.a_desired = init_a
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
self.prev_accel_clip = [ACCEL_MIN, ACCEL_MAX]
self.v_model_error = 0.0
self.output_a_target = 0.0
self.output_should_stop = False
@@ -73,12 +73,12 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.solverExecutionTime = 0.0
@staticmethod
def parse_model(model_msg):
def parse_model(model_msg, model_error):
if (len(model_msg.position.x) == ModelConstants.IDX_N and
len(model_msg.velocity.x) == ModelConstants.IDX_N and
len(model_msg.acceleration.x) == ModelConstants.IDX_N):
x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x)
v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.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) - model_error
a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.acceleration.x)
j = np.zeros(len(T_IDXS_MPC))
else:
@@ -94,13 +94,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
def update(self, sm):
self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
if not self.mlsim:
self.mpc.mode = self.mode
LongitudinalPlannerSP.update(self, sm)
if dec_mpc_mode := self.get_mpc_mode():
self.mode = dec_mpc_mode
if not self.mlsim:
self.mpc.mode = dec_mpc_mode
if len(sm['carControl'].orientationNED) == 3:
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
@@ -137,7 +133,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
# Prevent divergence, smooth in current 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
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_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
self.output_should_stop = output_should_stop_mpc
else:

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
import jinja2
import os
from cereal import car
from openpilot.common.basedir import BASEDIR
from opendbc.car.interfaces import get_interface_attr
Ecu = car.CarParams.Ecu
CARS = get_interface_attr('CAR')
FW_VERSIONS = get_interface_attr('FW_VERSIONS')
FINGERPRINTS = get_interface_attr('FINGERPRINTS')
ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()}
FINGERPRINTS_PY_TEMPLATE = jinja2.Template("""
{%- if FINGERPRINTS[brand] %}
# ruff: noqa: E501
{% endif %}
{% if FW_VERSIONS[brand] %}
from opendbc.car.structs import CarParams
{% endif %}
from opendbc.car.{{brand}}.values import CAR
{% if FW_VERSIONS[brand] %}
Ecu = CarParams.Ecu
{% endif %}
{% if comments +%}
{{ comments | join() }}
{% endif %}
{% if FINGERPRINTS[brand] %}
FINGERPRINTS = {
{% for car, fingerprints in FINGERPRINTS[brand].items() %}
CAR.{{car.name}}: [{
{% for fingerprint in fingerprints %}
{% if not loop.first %}
{{ "{" }}
{% endif %}
{% for key, value in fingerprint.items() %}{{key}}: {{value}}{% if not loop.last %}, {% endif %}{% endfor %}
}{% if loop.last %}]{% endif %},
{% endfor %}
{% endfor %}
}
{% endif %}
FW_VERSIONS{% if not FW_VERSIONS[brand] %}: dict[str, dict[tuple, list[bytes]]]{% endif %} = {
{% for car, _ in FW_VERSIONS[brand].items() %}
CAR.{{car.name}}: {
{% for key, fw_versions in FW_VERSIONS[brand][car].items() %}
(Ecu.{{ECU_NAME[key[0]]}}, 0x{{"%0x" | format(key[1] | int)}}, \
{% if key[2] %}0x{{"%0x" | format(key[2] | int)}}{% else %}{{key[2]}}{% endif %}): [
{% for fw_version in (fw_versions + extra_fw_versions.get(car, {}).get(key, [])) | unique | sort %}
{{fw_version}},
{% endfor %}
],
{% endfor %}
},
{% endfor %}
}
""", trim_blocks=True)
def format_brand_fw_versions(brand, extra_fw_versions: None | dict[str, dict[tuple, list[bytes]]] = None):
extra_fw_versions = extra_fw_versions or {}
fingerprints_file = os.path.join(BASEDIR, f"opendbc/car/{brand}/fingerprints.py")
with open(fingerprints_file) as f:
comments = [line for line in f.readlines() if line.startswith("#") and "noqa" not in line]
with open(fingerprints_file, "w") as f:
f.write(FINGERPRINTS_PY_TEMPLATE.render(brand=brand, comments=comments, ECU_NAME=ECU_NAME,
FINGERPRINTS=FINGERPRINTS, FW_VERSIONS=FW_VERSIONS,
extra_fw_versions=extra_fw_versions))
if __name__ == "__main__":
for brand in FW_VERSIONS.keys():
format_brand_fw_versions(brand)

View File

@@ -13,7 +13,6 @@ from typing import NoReturn
from cereal import log, car
import cereal.messaging as messaging
from openpilot.system.hardware import HARDWARE
from openpilot.common.conversions import Conversions as CV
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process
@@ -37,11 +36,8 @@ RPY_INIT = np.array([0.0,0.0,0.0])
WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0])
HEIGHT_INIT = np.array([1.22])
# These values are needed to accommodate the model frame in the narrow cam
if HARDWARE.get_device_type() == 'mici':
PITCH_LIMITS = np.array([-0.143101, 0.22235988])
else:
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
# These values are needed to accommodate the model frame in the narrow cam of the C3
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235])
DEBUG = os.getenv("DEBUG") is not None

View File

@@ -82,12 +82,6 @@ class PointBuckets:
total_points_valid = self.__len__() >= self.min_points_total
return individual_buckets_valid and total_points_valid
def get_valid_percent(self) -> int:
total_points_perc = min(self.__len__() / self.min_points_total * 100, 100)
individual_buckets_perc = min(min(len(v) / min_pts * 100 for v, min_pts in
zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)), 100)
return int((total_points_perc + individual_buckets_perc) / 2)
def is_calculable(self) -> bool:
return all(len(v) > 0 for v in self.buckets.values())

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,
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,
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.window_sec = window_sec
self.okay_window_sec = okay_window_sec
@@ -172,6 +173,7 @@ class LateralLagEstimator:
self.min_confidence = min_confidence
self.max_lat_accel = max_lat_accel
self.max_lat_accel_diff = max_lat_accel_diff
self.enabled = enabled
self.t = 0.0
self.lat_active = False
@@ -206,7 +208,7 @@ class LateralLagEstimator:
liveDelay = msg.liveDelay
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:
liveDelay.status = log.LiveDelayData.Status.invalid
else:
@@ -227,8 +229,6 @@ class LateralLagEstimator:
liveDelay.lateralDelayEstimateStd = 0.0
liveDelay.validBlocks = self.block_avg.valid_blocks
liveDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) //
(self.min_valid_block_count * self.block_size), 100)
if debug:
liveDelay.points = self.block_avg.values.flatten().tolist()
@@ -369,7 +369,10 @@ def main():
params = Params()
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:
lag, valid_blocks = initial_lag_params
lag_learner.reset(lag, valid_blocks)

View File

@@ -94,7 +94,6 @@ class TestLagd:
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
assert msg.liveDelay.validBlocks == 0
assert msg.liveDelay.calPerc == 0
def test_estimator_basics(self, subtests):
for lag_frames in range(5):
@@ -108,7 +107,18 @@ class TestLagd:
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_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
def test_estimator_masking(self):
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
@@ -117,7 +127,6 @@ class TestLagd:
msg = estimator.get_msg(True)
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.calPerc == 100
@pytest.mark.skipif(PC, reason="only on device")
@pytest.mark.timeout(60)

View File

@@ -1,25 +0,0 @@
from cereal import car
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
def test_cal_percent():
est = TorqueEstimator(car.CarParams())
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 0
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
est.filtered_points.buckets_min_points.values(), strict=True):
for _ in range(int(min_pts)):
est.filtered_points.add_point((low + high) / 2.0, 0.0)
# enough bucket points, but not enough total points
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
# add enough points to bucket with most capacity
key = list(est.filtered_points.buckets)[0]
for _ in range(est.min_points_total - len(est.filtered_points)):
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 100

View File

@@ -11,8 +11,6 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
HISTORY = 5 # secs
POINTS_PER_BUCKET = 1500
MIN_POINTS_TOTAL = 4000
@@ -51,10 +49,8 @@ class TorqueBuckets(PointBuckets):
break
class TorqueEstimator(ParameterEstimator, LagdToggle):
class TorqueEstimator(ParameterEstimator):
def __init__(self, CP, decimated=False, track_all_points=False):
super().__init__()
self.CP = CP
self.hist_len = int(HISTORY / DT_MDL)
self.lag = 0.0
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":
self.calibrator.feed_live_calib(msg)
elif which == "liveDelay":
self.lag = self.lagd_torqued_main(self.CP, msg)
self.lag = msg.lateralDelay
# calculate lateral accel from past steering torque
elif which == "livePose":
if len(self.raw_points['steer_torque']) == self.hist_len:
@@ -237,7 +233,6 @@ class TorqueEstimator(ParameterEstimator, LagdToggle):
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x)
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x)
liveTorqueParameters.totalBucketPoints = len(self.filtered_points)
liveTorqueParameters.calPerc = self.filtered_points.get_valid_percent()
liveTorqueParameters.decay = self.decay
liveTorqueParameters.maxResets = self.resets
return msg

View File

@@ -56,15 +56,17 @@ for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
tg_compile(flags, model_name)
# Compile BIG model if USB GPU is available
if "USBGPU" in os.environ:
import subprocess
# 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)
if b"AMD" in devs:
print("USB GPU detected... building")
flags = "AMD=1 AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0"
bp = tg_compile(flags, "big_driving_policy")
bv = tg_compile(flags, "big_driving_vision")
lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially
else:
print("USB GPU not detected... skipping")
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)
if b"AMD" in devs:
del Device
print("USB GPU detected... building")
flags = "AMD=1 AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0"
bp = tg_compile(flags, "big_driving_policy")
bv = tg_compile(flags, "big_driving_vision")
lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially
else:
print("USB GPU not detected... skipping")

View File

@@ -14,6 +14,7 @@ import pickle
import ctypes
import numpy as np
from pathlib import Path
from setproctitle import setproctitle
from cereal import messaging
from cereal.messaging import PubMaster, SubMaster
@@ -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.selfdrive.modeld.models.commonmodel_pyx import CLContext, MonitoringModelFrame
from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid
from openpilot.system import sentry
MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE
CALIB_LEN = 3
@@ -131,8 +133,12 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts:
def main():
setproctitle(PROCESS_NAME)
config_realtime_process(7, 5)
sentry.set_tag("daemon", PROCESS_NAME)
cloudlog.bind(daemon=PROCESS_NAME)
cl_context = CLContext()
model = ModelState(cl_context)
cloudlog.warning("models loaded, dmonitoringmodeld starting")
@@ -174,4 +180,7 @@ if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
cloudlog.warning("got SIGINT")
cloudlog.warning(f"child {PROCESS_NAME} got SIGINT")
except Exception:
sentry.capture_exception()
raise

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.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
fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T)

View File

@@ -19,6 +19,7 @@ import numpy as np
import cereal.messaging as messaging
from cereal import car, log
from pathlib import Path
from setproctitle import setproctitle
from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from opendbc.car.car_helpers import get_demo_car_params
@@ -28,15 +29,14 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.system import sentry
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.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.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
PROCESS_NAME = "selfdrive.modeld.modeld"
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'
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
LAT_SMOOTH_SECONDS = 0.1
LONG_SMOOTH_SECONDS = 0.3
LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.0
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)
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],
plan[:,Plan.ORIENTATION_RATE][:,2],
ModelConstants.T_IDXS,
v_ego,
lat_action_t)
desired_curvature = model_output['desired_curvature'][0, 0]
if v_ego > MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS)
else:
@@ -90,21 +86,10 @@ class ModelState:
prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self, context: CLContext):
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
with open(VISION_METADATA_PATH, 'rb') as f:
vision_metadata = pickle.load(f)
self.vision_input_shapes = vision_metadata['input_shapes']
self.vision_input_names = list(self.vision_input_shapes.keys())
self.vision_output_slices = vision_metadata['output_slices']
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
with open(POLICY_METADATA_PATH, 'rb') as f:
policy_metadata = pickle.load(f)
self.policy_input_shapes = policy_metadata['input_shapes']
self.policy_output_slices = policy_metadata['output_slices']
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
self.frames = {name: DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP) for name in self.vision_input_names}
self.frames = {
'input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP),
'big_input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP)
}
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
@@ -121,6 +106,18 @@ class ModelState:
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
}
with open(VISION_METADATA_PATH, 'rb') as f:
vision_metadata = pickle.load(f)
self.vision_input_shapes = vision_metadata['input_shapes']
self.vision_output_slices = vision_metadata['output_slices']
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
with open(POLICY_METADATA_PATH, 'rb') as f:
policy_metadata = pickle.load(f)
self.policy_input_shapes = policy_metadata['input_shapes']
self.policy_output_slices = policy_metadata['output_slices']
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
# img buffers are managed in openCL transform code
self.vision_inputs: dict[str, Tensor] = {}
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
@@ -138,7 +135,7 @@ class ModelState:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
return parsed_model_outputs
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray,
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire'][0] = 0
@@ -151,7 +148,8 @@ class ModelState:
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()),
'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())}
if TICI and not USBGPU:
# The imgs tensors are backed by opencl memory, only need init once
@@ -179,7 +177,7 @@ class ModelState:
# 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,:] = 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}
if SEND_RAW_PRED:
@@ -191,6 +189,9 @@ class ModelState:
def main(demo=False):
cloudlog.warning("modeld init")
sentry.set_tag("daemon", PROCESS_NAME)
cloudlog.bind(daemon=PROCESS_NAME)
setproctitle(PROCESS_NAME)
if not USBGPU:
# USB GPU currently saturates a core so can't do this yet,
# also need to move the aux USB interrupts for good timings
@@ -253,8 +254,6 @@ def main(demo=False):
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
cloudlog.info("modeld got CarParams: %s", CP.brand)
modeld_lagd = LagdToggle()
# TODO this needs more thought, use .2s extra for now to estimate other delays
# TODO Move smooth seconds to action function
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
@@ -300,7 +299,7 @@ def main(demo=False):
is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["roadCameraState"].frameId
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)
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
@@ -329,16 +328,14 @@ def main(demo=False):
if prepare_only:
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
inputs:dict[str, np.ndarray] = {
'desire': vec_desire,
'traffic_convention': traffic_convention,
'lateral_control_params': lateral_control_params,
}
}
mt1 = time.perf_counter()
model_output = model.run(bufs, transforms, inputs, prepare_only)
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
mt2 = time.perf_counter()
model_execution_time = mt2 - mt1
@@ -378,4 +375,7 @@ if __name__ == "__main__":
args = parser.parse_args()
main(demo=args.demo)
except KeyboardInterrupt:
cloudlog.warning("got SIGINT")
cloudlog.warning(f"child {PROCESS_NAME} got SIGINT")
except Exception:
sentry.capture_exception()
raise

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('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('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_binary_crossentropy('meta', outs)
return outs
@@ -101,10 +95,17 @@ class Parser:
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,
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:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
return outs

View File

@@ -585,6 +585,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
},
EventName.noGps: {
ET.PERMANENT: Alert(
"Poor GPS reception",
"Ensure device has a clear view of the sky",
AlertStatus.normal, AlertSize.mid,
Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=600.)
},
EventName.tooDistracted: {

View File

@@ -382,16 +382,16 @@ class SelfdriveD(CruiseHelper):
if (planner_fcw or model_fcw) and not self.CP.notCar:
self.events.add(EventName.fcw)
# GPS checks
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
self.events.add(EventName.noGps)
if gps_ok:
self.distance_traveled = 0
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
# TODO: fix simulator
if not SIMULATION or REPLAY:
# Not show in first 1.5 km to allow for driving out of garage. This event shows after 5 minutes
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
self.events.add(EventName.noGps)
if gps_ok:
self.distance_traveled = 0
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
if self.sm['modelV2'].frameDropPerc > 20:
self.events.add(EventName.modeldLagging)

20
selfdrive/test/ci_shell.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
OP_ROOT="$DIR/../../"
if [ -z "$BUILD" ]; then
docker pull ghcr.io/commaai/openpilot-base:latest
else
docker build --cache-from ghcr.io/commaai/openpilot-base:latest -t ghcr.io/commaai/openpilot-base:latest -f $OP_ROOT/Dockerfile.openpilot_base .
fi
docker run \
-it \
--rm \
--volume $OP_ROOT:$OP_ROOT \
--workdir $PWD \
--env PYTHONPATH=$OP_ROOT \
ghcr.io/commaai/openpilot-base:latest \
/bin/bash

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import os
import pickle
import sys
from collections import defaultdict
from typing import Any
@@ -190,44 +189,22 @@ def model_replay(lr, frs):
print("----------------- Model Timing -----------------")
print("------------------------------------------------")
print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f"))
assert timings_ok or PC
assert timings_ok
return msgs
def get_frames():
regen_cache = "--regen-cache" in sys.argv
frames_cache = '/tmp/model_replay_cache' if PC else '/data/model_replay_cache'
os.makedirs(frames_cache, exist_ok=True)
cache_name = f'{frames_cache}/{TEST_ROUTE}_{SEGMENT}_{START_FRAME}_{END_FRAME}.pkl'
if os.path.isfile(cache_name) and not regen_cache:
try:
print(f"Loading frames from cache {cache_name}")
return pickle.load(open(cache_name, "rb"))
except Exception as e:
print(f"Failed to load frames from cache {cache_name}: {e}")
frs = {
'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
}
for fr in frs.values():
for fidx in range(START_FRAME, END_FRAME):
fr.get(fidx)
fr.it = None
print(f"Dumping frame cache {cache_name}")
pickle.dump(frs, open(cache_name, "wb"))
return frs
if __name__ == "__main__":
update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master')
replay_dir = os.path.dirname(os.path.abspath(__file__))
# load logs
lr = list(LogReader(get_url(TEST_ROUTE, SEGMENT, "rlog.zst")))
frs = get_frames()
frs = {
'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), readahead=True),
'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), readahead=True),
'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), readahead=True)
}
log_msgs = []
# run replays

View File

@@ -27,7 +27,7 @@ from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera
from openpilot.selfdrive.test.process_replay.migration import migrate_all
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
from openpilot.tools.lib.logreader import LogIterable
from openpilot.tools.lib.framereader import FrameReader
from openpilot.tools.lib.framereader import BaseFrameReader
# Numpy gives different results based on CPU features after version 19
NUMPY_TOLERANCE = 1e-7
@@ -209,7 +209,6 @@ class ProcessContainer:
streams_metas = available_streams(all_msgs)
for meta in streams_metas:
if meta.camera_state in self.cfg.vision_pubs:
assert frs[meta.camera_state].pix_fmt == 'nv12'
frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h)
vipc_server.create_buffers(meta.stream, 2, *frame_size)
vipc_server.start_listener()
@@ -225,7 +224,7 @@ class ProcessContainer:
def start(
self, params_config: dict[str, Any], environ_config: dict[str, Any],
all_msgs: LogIterable, frs: dict[str, FrameReader] | None,
all_msgs: LogIterable, frs: dict[str, BaseFrameReader] | None,
fingerprint: str | None, capture_output: bool
):
with self.prefix as p:
@@ -267,7 +266,7 @@ class ProcessContainer:
self.prefix.clean_dirs()
self._clean_env()
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, FrameReader] | None) -> list[capnp._DynamicStructReader]:
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, BaseFrameReader] | None) -> list[capnp._DynamicStructReader]:
assert self.rc and self.pm and self.sockets and self.process.proc
output_msgs = []
@@ -297,7 +296,7 @@ class ProcessContainer:
camera_state = getattr(m, m.which())
camera_meta = meta_from_camera_state(m.which())
assert frs is not None
img = frs[m.which()].get(camera_state.frameId)
img = frs[m.which()].get(camera_state.frameId, pix_fmt="nv12")[0]
self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(),
camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof)
self.msg_queue = []
@@ -656,7 +655,7 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
def replay_process(
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None,
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] = None,
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None,
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
) -> list[capnp._DynamicStructReader]:
@@ -684,7 +683,7 @@ def replay_process(
def _replay_multi_process(
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None, fingerprint: str | None,
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] | None, fingerprint: str | None,
custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool
) -> list[capnp._DynamicStructReader]:
if fingerprint is not None:

View File

@@ -1 +1 @@
de16c6fbe14e121c5e74cd944ce03a0e4672540d
9e2fe2942fbf77f24bccdbef15893831f9c0b390

View File

@@ -3,17 +3,40 @@ import os
import argparse
import time
import capnp
import numpy as np
from typing import Any
from collections.abc import Iterable
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr
from openpilot.selfdrive.test.process_replay.vision_meta import DRIVER_CAMERA_FRAME_SIZES
from openpilot.selfdrive.test.update_ci_routes import upload_route
from openpilot.tools.lib.framereader import FrameReader
from openpilot.tools.lib.framereader import FrameReader, BaseFrameReader, FrameType
from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log
from openpilot.tools.lib.openpilotci import get_url
class DummyFrameReader(BaseFrameReader):
def __init__(self, w: int, h: int, frame_count: int, pix_val: int):
self.pix_val = pix_val
self.w, self.h = w, h
self.frame_count = frame_count
self.frame_type = FrameType.raw
def get(self, idx, count=1, pix_fmt="rgb24"):
if pix_fmt == "rgb24":
shape = (self.h, self.w, 3)
elif pix_fmt == "nv12" or pix_fmt == "yuv420p":
shape = (int((self.h * self.w) * 3 / 2),)
else:
raise NotImplementedError
return [np.full(shape, self.pix_val, dtype=np.uint8) for _ in range(count)]
@staticmethod
def zero_dcamera():
return DummyFrameReader(*DRIVER_CAMERA_FRAME_SIZES[("tici", "ar0231")], 1200, 0)
def regen_segment(
lr: LogIterable, frs: dict[str, Any] = None,
@@ -41,7 +64,7 @@ def setup_data_readers(
frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc"))
if needs_driver_cam:
if dummy_driver_cam:
frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) # Use fcam as dummy
frs['driverCameraState'] = DummyFrameReader.zero_dcamera()
else:
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."

View File

@@ -19,6 +19,7 @@ from openpilot.tools.lib.logreader import LogReader, save_log
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
source_segments = [
("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.COMMA_BODY
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.HYUNDAI_KIA_EV6 (+ QCOM GPS)
("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.TOYOTA_PRIUS
@@ -43,6 +44,7 @@ source_segments = [
]
segments = [
("BODY", "regen2F3C7259F1B|2025-04-08--23-00-23--0"),
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
@@ -63,7 +65,7 @@ segments = [
]
# dashcamOnly makes don't need to be tested until a full port is done
excluded_interfaces = ["mock", "body"]
excluded_interfaces = ["mock", "tesla"]
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
@@ -251,7 +253,7 @@ if __name__ == "__main__":
continue
# to speed things up, we only test all segments on card
if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'):
if cfg.proc_name != 'card' and car_brand not in ('HYUNDAI', 'TOYOTA', 'HONDA', 'SUBARU', 'FORD', 'RIVIAN', 'TESLA'):
continue
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")

View File

@@ -1,6 +1,6 @@
from parameterized import parameterized
from openpilot.selfdrive.test.process_replay.regen import regen_segment
from openpilot.selfdrive.test.process_replay.regen import regen_segment, DummyFrameReader
from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled
from openpilot.tools.lib.openpilotci import get_url
from openpilot.tools.lib.logreader import LogReader
@@ -18,7 +18,7 @@ def ci_setup_data_readers(route, sidx):
lr = LogReader(get_url(route, sidx, "rlog.bz2"))
frs = {
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
'driverCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
'driverCameraState': DummyFrameReader.zero_dcamera()
}
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera.hevc"))

View File

@@ -112,7 +112,7 @@ if GetOption('extras'):
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
# keep installers small
assert f[0].get_size() < 1900*1e3, f[0].get_size()
assert f[0].get_size() < 1300*1e3, f[0].get_size()
# build watch3
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):

View File

@@ -4,12 +4,10 @@ from collections.abc import Callable
from enum import IntEnum
from openpilot.common.params import Params
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.label import gui_label
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
from openpilot.system.ui.lib.widget import Widget
HEADER_HEIGHT = 80
HEAD_BUTTON_FONT_SIZE = 40
@@ -27,9 +25,8 @@ class HomeLayoutState(IntEnum):
ALERTS = 2
class HomeLayout(Widget):
class HomeLayout:
def __init__(self):
super().__init__()
self.params = Params()
self.update_alert = UpdateAlert()
@@ -50,10 +47,6 @@ class HomeLayout(Widget):
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10)
self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10)
self._prime_widget = PrimeWidget()
self._setup_widget = SetupWidget()
self._exp_mode_button = ExperimentalModeButton()
self._setup_callbacks()
def _setup_callbacks(self):
@@ -66,7 +59,9 @@ class HomeLayout(Widget):
def _set_state(self, state: HomeLayoutState):
self.current_state = state
def _render(self, rect: rl.Rectangle):
def render(self, rect: rl.Rectangle):
self._update_layout_rects(rect)
current_time = time.time()
if current_time - self.last_refresh >= REFRESH_INTERVAL:
self._refresh()
@@ -83,16 +78,16 @@ class HomeLayout(Widget):
elif self.current_state == HomeLayoutState.ALERTS:
self._render_alerts_view()
def _update_layout_rects(self):
def _update_layout_rects(self, rect: rl.Rectangle):
self.header_rect = rl.Rectangle(
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
)
content_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
content_y = rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
content_height = rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
self.content_rect = rl.Rectangle(
self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height
rect.x + CONTENT_MARGIN, content_y, rect.width - 2 * CONTENT_MARGIN, content_height
)
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
@@ -175,25 +170,28 @@ class HomeLayout(Widget):
self.offroad_alert.render(self.content_rect)
def _render_left_column(self):
self._prime_widget.render(self.left_column_rect)
rl.draw_rectangle_rounded(self.left_column_rect, 0.02, 10, PRIME_BG_COLOR)
gui_label(self.left_column_rect, "Prime Widget", 48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
def _render_right_column(self):
exp_height = 125
widget_height = (self.right_column_rect.height - SPACING) // 2
exp_rect = rl.Rectangle(
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, widget_height
)
self._exp_mode_button.render(exp_rect)
rl.draw_rectangle_rounded(exp_rect, 0.02, 10, PRIME_BG_COLOR)
gui_label(exp_rect, "Experimental Mode", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
setup_rect = rl.Rectangle(
self.right_column_rect.x,
self.right_column_rect.y + exp_height + SPACING,
self.right_column_rect.y + widget_height + SPACING,
self.right_column_rect.width,
self.right_column_rect.height - exp_height - SPACING,
widget_height,
)
self._setup_widget.render(setup_rect)
rl.draw_rectangle_rounded(setup_rect, 0.02, 10, PRIME_BG_COLOR)
gui_label(setup_rect, "Setup", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
def _refresh(self):
# TODO: implement _update_state with a timer
self.update_available = self.update_alert.refresh()
self.alert_count = self.offroad_alert.refresh()
self._update_state_priority(self.update_available, self.alert_count > 0)

View File

@@ -1,12 +1,10 @@
import pyray as rl
from enum import IntEnum
import cereal.messaging as messaging
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
from openpilot.selfdrive.ui.layouts.home import HomeLayout
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
from openpilot.system.ui.lib.widget import Widget
class MainState(IntEnum):
@@ -15,15 +13,14 @@ class MainState(IntEnum):
ONROAD = 2
class MainLayout(Widget):
class MainLayout:
def __init__(self):
super().__init__()
self._pm = messaging.PubMaster(['userFlag'])
self._sidebar = Sidebar()
self._sidebar_visible = True
self._current_mode = MainState.HOME
self._prev_onroad = False
self._window_rect = None
self._current_callback: callable | None = None
# Initialize layouts
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
@@ -34,23 +31,32 @@ class MainLayout(Widget):
# Set callbacks
self._setup_callbacks()
def _render(self, _):
def render(self, rect):
self._current_callback = None
self._update_layout_rects(rect)
self._handle_onroad_transition()
self._render_main_content()
self._handle_input()
if self._current_callback:
self._current_callback()
def _setup_callbacks(self):
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
on_flag=self._on_flag_clicked)
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
device.add_interactive_timeout_callback(self._set_mode_for_state)
self._sidebar.set_callbacks(
on_settings=lambda: setattr(self, '_current_callback', self._on_settings_clicked),
on_flag=lambda: setattr(self, '_current_callback', self._on_flag_clicked),
)
self._layouts[MainState.SETTINGS].set_callbacks(
on_close=lambda: setattr(self, '_current_callback', self._set_mode_for_state)
)
def _update_layout_rects(self):
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
def _update_layout_rects(self, rect):
self._window_rect = rect
self._sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
x_offset = SIDEBAR_WIDTH if self._sidebar_visible else 0
self._content_rect = rl.Rectangle(rect.y + x_offset, rect.y, rect.width - x_offset, rect.height)
def _handle_onroad_transition(self):
if ui_state.started != self._prev_onroad:
@@ -60,34 +66,31 @@ class MainLayout(Widget):
def _set_mode_for_state(self):
if ui_state.started:
# Don't hide sidebar from interactive timeout
if self._current_mode != MainState.ONROAD:
self._sidebar.set_visible(False)
self._current_mode = MainState.ONROAD
self._sidebar_visible = False
else:
self._current_mode = MainState.HOME
self._sidebar.set_visible(True)
def open_settings(self, panel_type: PanelType):
self._layouts[MainState.SETTINGS].set_current_panel(panel_type)
self._current_mode = MainState.SETTINGS
self._sidebar.set_visible(False)
self._sidebar_visible = True
def _on_settings_clicked(self):
self.open_settings(PanelType.DEVICE)
self._current_mode = MainState.SETTINGS
self._sidebar_visible = False
def _on_flag_clicked(self):
user_flag = messaging.new_message('userFlag')
user_flag.valid = True
self._pm.send('userFlag', user_flag)
def _on_onroad_clicked(self):
self._sidebar.set_visible(not self._sidebar.is_visible)
pass
def _render_main_content(self):
# Render sidebar
if self._sidebar.is_visible:
if self._sidebar_visible:
self._sidebar.render(self._sidebar_rect)
content_rect = self._content_rect if self._sidebar.is_visible else self._rect
content_rect = self._content_rect if self._sidebar_visible else self._window_rect
self._layouts[self._current_mode].render(content_rect)
def _handle_input(self):
if self._current_mode != MainState.ONROAD or not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
return
mouse_pos = rl.get_mouse_position()
if rl.check_collision_point_rec(mouse_pos, self._content_rect):
self._sidebar_visible = not self._sidebar_visible

View File

@@ -1,17 +1,19 @@
import pyray as rl
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
from openpilot.system.ui.widgets.network import WifiManagerUI
class NetworkLayout(Widget):
class NetworkLayout:
def __init__(self):
super().__init__()
self.wifi_manager = WifiManagerWrapper()
self.wifi_ui = WifiManagerUI(self.wifi_manager)
def _render(self, rect: rl.Rectangle):
def render(self, rect: rl.Rectangle):
self.wifi_ui.render(rect)
@property
def require_full_screen(self):
return self.wifi_ui.require_full_screen
def shutdown(self):
self.wifi_manager.shutdown()

View File

@@ -1,8 +1,5 @@
from openpilot.system.ui.lib.list_view import toggle_item
from openpilot.system.ui.lib.scroller import Scroller
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.lib.list_view import ListView, toggle_item
from openpilot.common.params import Params
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
# Description constants
DESCRIPTIONS = {
@@ -11,16 +8,11 @@ DESCRIPTIONS = {
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
),
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
'ssh_key': (
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
),
}
class DeveloperLayout(Widget):
class DeveloperLayout:
def __init__(self):
super().__init__()
self._params = Params()
items = [
toggle_item(
@@ -29,7 +21,6 @@ class DeveloperLayout(Widget):
initial_state=self._params.get_bool("AdbEnabled"),
callback=self._on_enable_adb,
),
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
toggle_item(
"Joystick Debug Mode",
description=DESCRIPTIONS["joystick_debug_mode"],
@@ -50,10 +41,10 @@ class DeveloperLayout(Widget):
),
]
self._scroller = Scroller(items, line_separator=True, spacing=0)
self._list_widget = ListView(items)
def _render(self, rect):
self._scroller.render(rect)
def render(self, rect):
self._list_widget.render(rect)
def _on_enable_adb(self): pass
def _on_joystick_debug_mode(self): pass

View File

@@ -1,150 +1,47 @@
import os
import json
from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.list_view import ListView, text_item, button_item
from openpilot.common.params import Params
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.list_view import text_item, button_item, dual_button_item
from openpilot.system.ui.lib.scroller import Scroller
from openpilot.system.ui.lib.widget import Widget, DialogResult
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
from openpilot.system.ui.widgets.html_render import HtmlRenderer
# Description constants
DESCRIPTIONS = {
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
'reset_calibration': (
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
),
'review_guide': "Review the rules, features, and limitations of openpilot",
}
class DeviceLayout(Widget):
class DeviceLayout:
def __init__(self):
super().__init__()
self._params = Params()
self._select_language_dialog: MultiOptionDialog | None = None
self._driver_camera: DriverCameraDialog | None = None
self._pair_device_dialog: PairingDialog | None = None
self._fcc_dialog: HtmlRenderer | None = None
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
dongle_id = self._params.get("DongleId", encoding="utf-8") or "N/A"
serial = self._params.get("HardwareSerial") or "N/A"
params = Params()
dongle_id = params.get("DongleId", encoding="utf-8") or "N/A"
serial = params.get("HardwareSerial") or "N/A"
items = [
text_item("Dongle ID", dongle_id),
text_item("Serial", serial),
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory),
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], self._on_pair_device),
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], self._on_driver_camera),
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], self._on_reset_calibration),
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
]
regulatory_btn.set_visible(TICI)
return items
def _render(self, rect):
self._scroller.render(rect)
if TICI:
items.append(button_item("Regulatory", "VIEW", callback=self._on_regulatory))
def _show_language_selection(self):
try:
languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")
with open(languages_file, encoding='utf-8') as f:
languages = json.load(f)
items.append(button_item("Change Language", "CHANGE", callback=self._on_change_language))
self._select_language_dialog = MultiOptionDialog("Select a language", languages)
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
except FileNotFoundError:
pass
self._list_widget = ListView(items)
def _handle_language_selection(self, result: int):
if result == 1 and self._select_language_dialog:
selected_language = self._select_language_dialog.selection
self._params.put("LanguageSetting", selected_language)
self._select_language_dialog = None
def _show_driver_camera(self):
if not self._driver_camera:
self._driver_camera = DriverCameraDialog()
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))
def _reset_calibration_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration"))
return
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"),
callback=self._reset_calibration,
)
def _reset_calibration(self, result: int):
if ui_state.engaged or result != DialogResult.CONFIRM:
return
self._params.remove("CalibrationParams")
self._params.remove("LiveTorqueParameters")
self._params.remove("LiveParameters")
self._params.remove("LiveParametersV2")
self._params.remove("LiveDelay")
self._params.put_bool("OnroadCycleRequested", True)
def _reboot_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot"))
return
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"),
callback=self._perform_reboot,
)
def _perform_reboot(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoReboot", True)
def _power_off_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off"))
return
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"),
callback=self._perform_power_off,
)
def _perform_power_off(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoShutdown", True)
def _pair_device(self):
if not self._pair_device_dialog:
self._pair_device_dialog = PairingDialog()
gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None))
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog,
callback=lambda result: setattr(self, '_fcc_dialog', None),
)
def render(self, rect):
self._list_widget.render(rect)
def _on_pair_device(self): pass
def _on_driver_camera(self): pass
def _on_reset_calibration(self): pass
def _on_review_training_guide(self): pass
def _on_regulatory(self): pass
def _on_change_language(self): pass

View File

@@ -1,180 +0,0 @@
import pyray as rl
import json
import time
import threading
from openpilot.common.api import Api, api_get
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.widget import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
TITLE = "Firehose Mode"
DESCRIPTION = (
"openpilot learns to drive by watching humans, like you, drive.\n\n"
+ "Firehose Mode allows you to maximize your training data uploads to improve "
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
)
INSTRUCTIONS = (
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
+ "Frequently Asked Questions\n\n"
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
+ "What's a good USB-C adapter? Any fast phone or laptop charger should be fine.\n\n"
+ "Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training."
)
class FirehoseLayout(Widget):
PARAM_KEY = "ApiCache_FirehoseStats"
GREEN = rl.Color(46, 204, 113, 255)
RED = rl.Color(231, 76, 60, 255)
GRAY = rl.Color(68, 68, 68, 255)
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
UPDATE_INTERVAL = 30 # seconds
def __init__(self):
super().__init__()
self.params = Params()
self.segment_count = self._get_segment_count()
self.scroll_panel = GuiScrollPanel()
self.running = True
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
self.update_thread.start()
self.last_update_time = 0
def _get_segment_count(self) -> int:
stats = self.params.get(self.PARAM_KEY, encoding='utf8')
if not stats:
return 0
try:
return int(json.loads(stats).get("firehose", 0))
except Exception:
cloudlog.exception(f"Failed to decode firehose stats: {stats}")
return 0
def __del__(self):
self.running = False
if self.update_thread and self.update_thread.is_alive():
self.update_thread.join(timeout=1.0)
def _render(self, rect: rl.Rectangle):
# Calculate content dimensions
content_width = rect.width - 80
content_height = self._calculate_content_height(int(content_width))
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
# Handle scrolling and render with clipping
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
self._render_content(rect, scroll_offset)
rl.end_scissor_mode()
def _calculate_content_height(self, content_width: int) -> int:
height = 80 # Top margin
# Title
height += 100 + 40
# Description
desc_font = gui_app.font(FontWeight.NORMAL)
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
height += len(desc_lines) * 45 + 40
# Status section
height += 32 # Separator
status_text, _ = self._get_status()
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
height += len(status_lines) * 60 + 20
# Contribution count (if available)
if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
height += len(contrib_lines) * 52 + 20
# Instructions section
height += 32 # Separator
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
height += len(inst_lines) * 40 + 40 # Bottom margin
return height
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
x = int(rect.x + 40)
y = int(rect.y + 40 + scroll_offset.y)
w = int(rect.width - 80)
# Title
title_font = gui_app.font(FontWeight.MEDIUM)
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
y += 140
# Description
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
y += 40
# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
# Status
status_text, status_color = self._get_status()
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
y += 20
# Contribution count (if available)
if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20
# Separator
rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 30
# Instructions
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
wrapped = wrap_text(font, text, size, width)
for line in wrapped:
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
y += size
return y
def _get_status(self) -> tuple[str, rl.Color]:
network_type = ui_state.sm["deviceState"].networkType
network_metered = ui_state.sm["deviceState"].networkMetered
if not network_metered and network_type != 0: # Not metered and connected
return "ACTIVE", self.GREEN
else:
return "INACTIVE: connect to an unmetered network", self.RED
def _fetch_firehose_stats(self):
try:
dongle_id = self.params.get("DongleId", encoding='utf8')
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return
identity_token = Api(dongle_id).get_token()
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
if response.status_code == 200:
data = response.json()
self.segment_count = data.get("firehose", 0)
self.params.put(self.PARAM_KEY, json.dumps(data))
except Exception as e:
cloudlog.error(f"Failed to fetch firehose stats: {e}")
def _update_loop(self):
while self.running:
if not ui_state.started:
self._fetch_firehose_stats()
time.sleep(self.UPDATE_INTERVAL)

View File

@@ -2,15 +2,15 @@ import pyray as rl
from dataclasses import dataclass
from enum import IntEnum
from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.label import gui_text_box
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.selfdrive.ui.layouts.network import NetworkLayout
from openpilot.system.ui.lib.widget import Widget
# Import individual panels
@@ -18,8 +18,9 @@ SETTINGS_CLOSE_TEXT = "X"
# Constants
SIDEBAR_WIDTH = 500
CLOSE_BTN_SIZE = 200
NAV_BTN_HEIGHT = 110
NAV_BTN_HEIGHT = 80
PANEL_MARGIN = 50
SCROLL_SPEED = 30
# Colors
SIDEBAR_COLOR = rl.BLACK
@@ -28,6 +29,7 @@ CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255)
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
TEXT_PRESSED = rl.Color(173, 173, 173, 255)
class PanelType(IntEnum):
@@ -43,22 +45,23 @@ class PanelType(IntEnum):
class PanelInfo:
name: str
instance: object
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
button_rect: rl.Rectangle
class SettingsLayout(Widget):
class SettingsLayout:
def __init__(self):
super().__init__()
self._params = Params()
self._current_panel = PanelType.DEVICE
self._max_scroll = 0.0
# Panel configuration
self._panels = {
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
PanelType.NETWORK: PanelInfo("Network", NetworkLayout()),
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
PanelType.DEVICE: PanelInfo("Device", DeviceLayout(), rl.Rectangle(0, 0, 0, 0)),
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout(), rl.Rectangle(0, 0, 0, 0)),
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout(), rl.Rectangle(0, 0, 0, 0)),
PanelType.FIREHOSE: PanelInfo("Firehose", None, rl.Rectangle(0, 0, 0, 0)),
PanelType.NETWORK: PanelInfo("Network", NetworkLayout(), rl.Rectangle(0, 0, 0, 0)),
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout(), rl.Rectangle(0, 0, 0, 0)),
}
self._font_medium = gui_app.font(FontWeight.MEDIUM)
@@ -70,7 +73,7 @@ class SettingsLayout(Widget):
def set_callbacks(self, on_close: Callable):
self._close_callback = on_close
def _render(self, rect: rl.Rectangle):
def render(self, rect: rl.Rectangle):
# Calculate layout
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
@@ -79,6 +82,9 @@ class SettingsLayout(Widget):
self._draw_sidebar(sidebar_rect)
self._draw_current_panel(panel_rect)
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
self.handle_mouse_release(rl.get_mouse_position())
def _draw_sidebar(self, rect: rl.Rectangle):
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
@@ -103,9 +109,17 @@ class SettingsLayout(Widget):
self._close_btn_rect = close_btn_rect
# Navigation buttons
y = rect.y + 300
nav_start_y = rect.y + 300
button_spacing = 20
i = 0
for panel_type, panel_info in self._panels.items():
button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
button_rect = rl.Rectangle(
rect.x + 50,
nav_start_y + i * (NAV_BTN_HEIGHT + button_spacing),
rect.width - 150, # Right-aligned with margin
NAV_BTN_HEIGHT,
)
# Button styling
is_selected = panel_type == self._current_panel
@@ -119,8 +133,7 @@ class SettingsLayout(Widget):
# Store button rect for click detection
panel_info.button_rect = button_rect
y += NAV_BTN_HEIGHT
i += 1
def _draw_current_panel(self, rect: rl.Rectangle):
rl.draw_rectangle_rounded(
@@ -131,8 +144,17 @@ class SettingsLayout(Widget):
panel = self._panels[self._current_panel]
if panel.instance:
panel.instance.render(content_rect)
else:
gui_text_box(
content_rect,
f"Demo {self._panels[self._current_panel].name} Panel",
font_size=170,
color=rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
)
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
def handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
# Check close button
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
if self._close_callback:
@@ -142,15 +164,20 @@ class SettingsLayout(Widget):
# Check navigation buttons
for panel_type, panel_info in self._panels.items():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
self.set_current_panel(panel_type)
self._switch_to_panel(panel_type)
return True
return False
def set_current_panel(self, panel_type: PanelType):
def _switch_to_panel(self, panel_type: PanelType):
if panel_type != self._current_panel:
self._current_panel = panel_type
def set_current_panel(self, index: int, param: str = ""):
panel_types = list(self._panels.keys())
if 0 <= index < len(panel_types):
self._switch_to_panel(panel_types[index])
def close_settings(self):
if self._close_callback:
self._close_callback()

View File

@@ -1,20 +1,7 @@
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.list_view import button_item, text_item
from openpilot.system.ui.lib.scroller import Scroller
from openpilot.system.ui.lib.widget import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
from openpilot.system.ui.lib.list_view import ListView, button_item, text_item
class SoftwareLayout(Widget):
class SoftwareLayout:
def __init__(self):
super().__init__()
self._params = Params()
items = self._init_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _init_items(self):
items = [
text_item("Current Version", ""),
button_item("Download", "CHECK", callback=self._on_download_update),
@@ -22,21 +9,13 @@ class SoftwareLayout(Widget):
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
]
return items
def _render(self, rect):
self._scroller.render(rect)
self._list_widget = ListView(items)
def render(self, rect):
self._list_widget.render(rect)
def _on_download_update(self): pass
def _on_install_update(self): pass
def _on_select_branch(self): pass
def _on_uninstall(self):
def handle_uninstall_confirmation(result):
if result == DialogResult.CONFIRM:
self._params.put_bool("DoUninstall", True)
gui_app.set_modal_overlay(
lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"),
callback=handle_uninstall_confirmation,
)
def _on_uninstall(self): pass

View File

@@ -1,6 +1,4 @@
from openpilot.system.ui.lib.list_view import multiple_button_item, toggle_item
from openpilot.system.ui.lib.scroller import Scroller
from openpilot.system.ui.lib.widget import Widget
from openpilot.system.ui.lib.list_view import ListView, toggle_item
from openpilot.common.params import Params
# Description constants
@@ -10,25 +8,18 @@ DESCRIPTIONS = {
"Your attention is required at all times to use this feature."
),
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
"LongitudinalPersonality": (
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
"your steering wheel distance button."
),
"IsLdwEnabled": (
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
"without a turn signal activated while driving over 31 mph (50 km/h)."
"without a turn signal activated while driving over 31 mph (50 km/h)."
),
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
"IsMetric": "Display speed in km/h instead of mph.",
"RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.",
}
class TogglesLayout(Widget):
class TogglesLayout:
def __init__(self):
super().__init__()
self._params = Params()
items = [
toggle_item(
@@ -48,15 +39,6 @@ class TogglesLayout(Widget):
self._params.get_bool("DisengageOnAccelerator"),
icon="disengage_on_accelerator.png",
),
multiple_button_item(
"Driving Personality",
DESCRIPTIONS["LongitudinalPersonality"],
buttons=["Aggressive", "Standard", "Relaxed"],
button_width=255,
callback=self._set_longitudinal_personality,
selected_index=int(self._params.get("LongitudinalPersonality") or 0),
icon="speed_limit.png"
),
toggle_item(
"Enable Lane Departure Warnings",
DESCRIPTIONS["IsLdwEnabled"],
@@ -75,21 +57,12 @@ class TogglesLayout(Widget):
self._params.get_bool("RecordFront"),
icon="monitoring.png",
),
toggle_item(
"Record Microphone Audio",
DESCRIPTIONS["RecordAudio"],
self._params.get_bool("RecordAudio"),
icon="microphone.png",
),
toggle_item(
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png"
),
]
self._scroller = Scroller(items, line_separator=True, spacing=0)
self._list_widget = ListView(items)
def _render(self, rect):
self._scroller.render(rect)
def _set_longitudinal_personality(self, button_index: int):
self._params.put("LongitudinalPersonality", str(button_index))
def render(self, rect):
self._list_widget.render(rect)

View File

@@ -4,9 +4,8 @@ from dataclasses import dataclass
from collections.abc import Callable
from cereal import log
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.widget import Widget
SIDEBAR_WIDTH = 300
METRIC_HEIGHT = 126
@@ -19,7 +18,6 @@ HOME_BTN = rl.Rectangle(60, 860, 180, 180)
ThermalStatus = log.DeviceState.ThermalStatus
NetworkType = log.DeviceState.NetworkType
# Color scheme
class Colors:
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
@@ -37,7 +35,6 @@ class Colors:
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
NETWORK_TYPES = {
NetworkType.none: "Offline",
NetworkType.wifi: "WiFi",
@@ -60,10 +57,8 @@ class MetricData:
self.value = value
self.color = color
class Sidebar(Widget):
class Sidebar:
def __init__(self):
super().__init__()
self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_strength = 0
@@ -77,7 +72,7 @@ class Sidebar(Widget):
self._font_regular = gui_app.font(FontWeight.NORMAL)
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
# Callbacks
# Callbacks
self._on_settings_click: Callable | None = None
self._on_flag_click: Callable | None = None
@@ -85,7 +80,9 @@ class Sidebar(Widget):
self._on_settings_click = on_settings
self._on_flag_click = on_flag
def _render(self, rect: rl.Rectangle):
def render(self, rect: rl.Rectangle):
self.update_state()
# Background
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
@@ -93,7 +90,9 @@ class Sidebar(Widget):
self._draw_network_indicator(rect)
self._draw_metrics(rect)
def _update_state(self):
self._handle_mouse_release()
def update_state(self):
sm = ui_state.sm
if not sm.updated['deviceState']:
return
@@ -135,7 +134,11 @@ class Sidebar(Widget):
else:
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
def _handle_mouse_release(self, mouse_pos: MousePos):
def _handle_mouse_release(self):
if not rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
return
mouse_pos = rl.get_mouse_position()
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
if self._on_settings_click:
self._on_settings_click()
@@ -145,7 +148,8 @@ class Sidebar(Widget):
def _draw_buttons(self, rect: rl.Rectangle):
mouse_pos = rl.get_mouse_position()
mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
# Settings button
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)

View File

@@ -1,99 +0,0 @@
from enum import IntEnum
import os
import threading
import time
from openpilot.common.api import Api, api_get
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
class PrimeType(IntEnum):
UNKNOWN = -2,
UNPAIRED = -1,
NONE = 0,
MAGENTA = 1,
LITE = 2,
BLUE = 3,
MAGENTA_NEW = 4,
PURPLE = 5,
class PrimeState:
FETCH_INTERVAL = 5.0 # seconds between API calls
API_TIMEOUT = 10.0 # seconds for API requests
SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread
def __init__(self):
self._params = Params()
self._lock = threading.Lock()
self.prime_type: PrimeType = self._load_initial_state()
self._running = False
self._thread = None
self.start()
def _load_initial_state(self) -> PrimeType:
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType", encoding='utf8')
try:
if prime_type_str is not None:
return PrimeType(int(prime_type_str))
except (ValueError, TypeError):
pass
return PrimeType.UNKNOWN
def _fetch_prime_status(self) -> None:
dongle_id = self._params.get("DongleId", encoding='utf8')
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
return
try:
identity_token = Api(dongle_id).get_token()
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token)
if response.status_code == 200:
data = response.json()
is_paired = data.get("is_paired", False)
prime_type = data.get("prime_type", 0)
self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED)
except Exception as e:
cloudlog.error(f"Failed to fetch prime status: {e}")
def set_type(self, prime_type: PrimeType) -> None:
with self._lock:
if prime_type != self.prime_type:
self.prime_type = prime_type
self._params.put("PrimeType", str(int(prime_type)))
cloudlog.info(f"Prime type updated to {prime_type}")
def _worker_thread(self) -> None:
while self._running:
self._fetch_prime_status()
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
if not self._running:
break
time.sleep(self.SLEEP_INTERVAL)
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
self._running = True
self._thread = threading.Thread(target=self._worker_thread, daemon=True)
self._thread.start()
def stop(self) -> None:
self._running = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=1.0)
def get_type(self) -> PrimeType:
with self._lock:
return self.prime_type
def is_prime(self) -> bool:
with self._lock:
return bool(self.prime_type > PrimeType.NONE)
def __del__(self):
self.stop()

View File

@@ -6,9 +6,9 @@ from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS
from openpilot.system.ui.lib.label import gui_text_box
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.widget import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
ALERT_MARGIN = 40
ALERT_PADDING = 60
ALERT_LINE_SPACING = 45
@@ -21,6 +21,7 @@ ALERT_FONT_BIG = 88
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
# Constants
ALERT_COLORS = {
log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
@@ -60,9 +61,8 @@ ALERT_CRITICAL_REBOOT = Alert(
)
class AlertRenderer(Widget):
class AlertRenderer:
def __init__(self):
super().__init__()
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
@@ -73,20 +73,18 @@ class AlertRenderer(Widget):
# Check if selfdriveState messages have stopped arriving
if not sm.updated['selfdriveState']:
recv_frame = sm.recv_frame['selfdriveState']
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
if (sm.frame - recv_frame) > 5 * DEFAULT_FPS:
# Check if waiting to start
if recv_frame < ui_state.started_frame:
return ALERT_STARTUP_PENDING
# 1. Never received selfdriveState since going onroad
waiting_for_startup = recv_frame < ui_state.started_frame
if waiting_for_startup and time_since_onroad > 5:
return ALERT_STARTUP_PENDING
# 2. Lost communication with selfdriveState after receiving it
if TICI and not waiting_for_startup:
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
if ss_missing > SELFDRIVE_STATE_TIMEOUT:
if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT:
return ALERT_CRITICAL_TIMEOUT
return ALERT_CRITICAL_REBOOT
# Handle selfdrive timeout
if TICI:
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
if ss_missing > SELFDRIVE_STATE_TIMEOUT:
if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT:
return ALERT_CRITICAL_TIMEOUT
return ALERT_CRITICAL_REBOOT
# No alert if size is none
if ss.alertSize == 0:
@@ -95,10 +93,10 @@ class AlertRenderer(Widget):
# Return current alert
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
def _render(self, rect: rl.Rectangle) -> bool:
alert = self.get_alert(ui_state.sm)
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster) -> None:
alert = self.get_alert(sm)
if not alert:
return False
return
alert_rect = self._get_alert_rect(rect, alert.size)
self._draw_background(alert_rect, alert)
@@ -110,14 +108,13 @@ class AlertRenderer(Widget):
alert_rect.height - 2 * ALERT_PADDING
)
self._draw_text(text_rect, alert)
return True
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
if size == log.SelfdriveState.AlertSize.full:
return rect
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
return rl.Rectangle(
rect.x + ALERT_MARGIN,

View File

@@ -1,6 +1,6 @@
import numpy as np
import pyray as rl
from collections.abc import Callable
from cereal import log
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
from openpilot.common.transformations.orientation import rot_from_euler
OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
@@ -25,9 +26,6 @@ BORDER_COLORS = {
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
}
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
class AugmentedRoadView(CameraView):
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
@@ -49,19 +47,11 @@ class AugmentedRoadView(CameraView):
self.alert_renderer = AlertRenderer()
self.driver_state_renderer = DriverStateRenderer()
# Callbacks
self._click_callback: Callable | None = None
def set_callbacks(self, on_click: Callable | None = None):
self._click_callback = on_click
def _render(self, rect):
def render(self, rect):
# Only render when system is started to avoid invalid data access
if not ui_state.started:
return
self._switch_stream_if_needed(ui_state.sm)
# Update calibration before rendering
self._update_calibration()
@@ -86,13 +76,13 @@ class AugmentedRoadView(CameraView):
)
# Render the base camera view
super()._render(rect)
super().render(rect)
# Draw all UI overlays
self.model_renderer.render(self._content_rect)
self._hud_renderer.render(self._content_rect)
if not self.alert_renderer.render(self._content_rect):
self.driver_state_renderer.render(self._content_rect)
self.model_renderer.draw(self._content_rect, ui_state.sm)
self._hud_renderer.draw(self._content_rect, ui_state.sm)
self.alert_renderer.draw(self._content_rect, ui_state.sm)
self.driver_state_renderer.draw(self._content_rect, ui_state.sm)
# Custom UI extension point - add custom overlays here
# Use self._content_rect for positioning within camera bounds
@@ -100,32 +90,10 @@ class AugmentedRoadView(CameraView):
# End clipping region
rl.end_scissor_mode()
# Handle click events if no HUD interaction occurred
if not self._hud_renderer.handle_mouse_event():
if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect):
self._click_callback()
def _draw_border(self, rect: rl.Rectangle):
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
def _switch_stream_if_needed(self, sm):
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
v_ego = sm['carState'].vEgo
if v_ego < WIDE_CAM_MAX_SPEED:
target = WIDE_CAM
elif v_ego > ROAD_CAM_MIN_SPEED:
target = ROAD_CAM
else:
# Hysteresis zone - keep current stream
target = self.stream_type
else:
target = ROAD_CAM
if self.stream_type != target:
self.switch_stream(target)
def _update_calibration(self):
# Update device camera if not already set
sm = ui_state.sm
@@ -161,7 +129,7 @@ class AugmentedRoadView(CameraView):
# Get camera configuration
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
is_wide_camera = self.stream_type == WIDE_CAM
is_wide_camera = self.stream_type == VisionStreamType.VISION_STREAM_WIDE_ROAD
intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics
calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib
zoom = 2.0 if is_wide_camera else 1.1
@@ -202,9 +170,9 @@ class AugmentedRoadView(CameraView):
])
video_transform = np.array([
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
[0.0, 0.0, 1.0]
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
[0.0, 0.0, 1.0]
])
self.model_renderer.set_transform(video_transform @ calib_transform)

View File

@@ -1,4 +1,3 @@
import platform
import numpy as np
import pyray as rl
@@ -7,21 +6,12 @@ from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
from openpilot.system.ui.lib.widget import Widget
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
VERSION = """
VERTEX_SHADER = """
#version 300 es
precision mediump float;
"""
if platform.system() == "Darwin":
VERSION = """
#version 330 core
"""
VERTEX_SHADER = VERSION + """
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
@@ -51,7 +41,9 @@ if TICI:
}
"""
else:
FRAME_FRAGMENT_SHADER = VERSION + """
FRAME_FRAGMENT_SHADER = """
#version 300 es
precision mediump float;
in vec2 fragTexCoord;
uniform sampler2D texture0;
uniform sampler2D texture1;
@@ -63,10 +55,8 @@ else:
}
"""
class CameraView(Widget):
class CameraView:
def __init__(self, name: str, stream_type: VisionStreamType):
super().__init__()
self._name = name
# Primary stream
self.client = VisionIpcClient(name, stream_type, conflate=True)
@@ -78,6 +68,7 @@ class CameraView(Widget):
self._target_stream_type: VisionStreamType | None = None
self._switching: bool = False
self._texture_needs_update = True
self.last_connection_attempt: float = 0.0
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
@@ -91,7 +82,7 @@ class CameraView(Widget):
self.egl_images: dict[int, EGLImage] = {}
self.egl_texture: rl.Texture | None = None
self._placeholder_color: rl.Color | None = None
self._placeholder_color : rl.Color | None = None
# Initialize EGL for zero-copy rendering on TICI
if TICI:
@@ -154,12 +145,12 @@ class CameraView(Widget):
zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0)
return np.array([
[zx, 0.0, 0.0],
[0.0, zy, 0.0],
[0.0, 0.0, 1.0]
[zx, 0.0, 0.0],
[0.0, zy, 0.0],
[0.0, 0.0, 1.0]
])
def _render(self, rect: rl.Rectangle):
def render(self, rect: rl.Rectangle):
if self._switching:
self._handle_switch()
@@ -239,7 +230,7 @@ class CameraView(Widget):
# Update textures with new frame data
if self._texture_needs_update:
y_data = self.frame.data[: self.frame.uv_offset]
uv_data = self.frame.data[self.frame.uv_offset:]
uv_data = self.frame.data[self.frame.uv_offset :]
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
@@ -274,7 +265,7 @@ class CameraView(Widget):
def _handle_switch(self) -> None:
"""Check if target stream is ready and switch immediately."""
if not self._target_client or not self._switching:
return
return
# Try to connect target if needed
if not self._target_client.is_connected():
@@ -286,28 +277,28 @@ class CameraView(Widget):
# Check if target has frames ready
target_frame = self._target_client.recv(timeout_ms=0)
if target_frame:
self.frame = target_frame # Update current frame to target frame
self.frame = target_frame # Update current frame to target frame
self._complete_switch()
def _complete_switch(self) -> None:
"""Instantly switch to target stream."""
cloudlog.debug(f"Switching to {self._target_stream_type}")
# Clean up current resources
if self.client:
del self.client
"""Instantly switch to target stream."""
cloudlog.debug(f"Switching to {self._target_stream_type}")
# Clean up current resources
if self.client:
del self.client
# Switch to target
self.client = self._target_client
self._stream_type = self._target_stream_type
self._texture_needs_update = True
# Switch to target
self.client = self._target_client
self._stream_type = self._target_stream_type
self._texture_needs_update = True
# Reset state
self._target_client = None
self._target_stream_type = None
self._switching = False
# Reset state
self._target_client = None
self._target_stream_type = None
self._switching = False
# Initialize textures for new stream
self._initialize_textures()
# Initialize textures for new stream
self._initialize_textures()
def _initialize_textures(self):
self._clear_textures()

View File

@@ -1,23 +1,20 @@
import numpy as np
import pyray as rl
from cereal import messaging
from msgq.visionipc import VisionStreamType
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.label import gui_label
class DriverCameraDialog(CameraView):
def __init__(self):
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
class DriverCameraView(CameraView):
def __init__(self, stream_type: VisionStreamType):
super().__init__("camerad", stream_type)
self.driver_state_renderer = DriverStateRenderer()
def _render(self, rect):
super()._render(rect)
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
return 1
def render(self, rect, sm):
super().render(rect)
if not self.frame:
gui_label(
@@ -27,15 +24,13 @@ class DriverCameraDialog(CameraView):
font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
)
return -1
return
self._draw_face_detection(rect)
self.driver_state_renderer.render(rect)
self._draw_face_detection(rect, sm)
self.driver_state_renderer.draw(rect, sm)
return -1
def _draw_face_detection(self, rect: rl.Rectangle) -> None:
driver_state = ui_state.sm["driverStateV2"]
def _draw_face_detection(self, rect: rl.Rectangle, sm) -> None:
driver_state = sm["driverStateV2"]
is_rhd = driver_state.wheelOnRightProb > 0.5
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
face_detect = driver_data.faceProb > 0.7
@@ -88,11 +83,12 @@ class DriverCameraDialog(CameraView):
if __name__ == "__main__":
gui_app.init_window("Driver Camera View")
sm = messaging.SubMaster(["selfdriveState", "driverStateV2", "driverMonitoringState"])
driver_camera_view = DriverCameraDialog()
driver_camera_view = DriverCameraView(VisionStreamType.VISION_STREAM_DRIVER)
try:
for _ in gui_app.render():
ui_state.update()
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
sm.update()
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height), sm)
finally:
driver_camera_view.close()

View File

@@ -3,19 +3,19 @@ import pyray as rl
from dataclasses import dataclass
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.widget import Widget
# Default 3D coordinates for face keypoints as a NumPy array
DEFAULT_FACE_KPTS_3D = np.array([
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
[-5.98, -51.20, 8.00],
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
[-5.98, -51.20, 8.00],
], dtype=np.float32)
# UI constants
@@ -31,7 +31,6 @@ SCALES_NEG = np.array([0.7, 0.4, 0.4], dtype=np.float32)
ARC_POINT_COUNT = 37 # Number of points in the arc
ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
@dataclass
class ArcData:
"""Data structure for arc rendering parameters."""
@@ -41,15 +40,14 @@ class ArcData:
height: float
thickness: float
class DriverStateRenderer(Widget):
class DriverStateRenderer:
def __init__(self):
super().__init__()
# Initial state with NumPy arrays
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
self.is_active = False
self.is_rhd = False
self.dm_fade_state = 0.0
self.state_updated = False
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
@@ -75,10 +73,14 @@ class DriverStateRenderer(Widget):
self.engaged_color = rl.Color(26, 242, 66, 255)
self.disengaged_color = rl.Color(139, 139, 139, 255)
self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and
ui_state.sm.seen['driverMonitoringState']))
def draw(self, rect, sm):
if not self._is_visible(sm):
return
self._update_state(sm, rect)
if not self.state_updated:
return
def _render(self, rect):
# Set opacity based on active state
opacity = 0.65 if self.is_active else 0.2
@@ -103,14 +105,18 @@ class DriverStateRenderer(Widget):
if self.v_arc_data:
rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color)
def _update_state(self):
def _is_visible(self, sm):
"""Check if the visualization should be rendered."""
return (sm.recv_frame['driverStateV2'] > ui_state.started_frame and
sm.seen['driverMonitoringState'] and
sm['selfdriveState'].alertSize == 0)
def _update_state(self, sm, rect):
"""Update the driver monitoring state based on model data"""
sm = ui_state.sm
if not sm.updated["driverMonitoringState"]:
if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or
self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height):
self._pre_calculate_drawing_elements()
self.last_rect = self._rect
if not sm.updated["driverMonitoringState"]:
if self.state_updated and (rect.x != self.last_rect.x or rect.y != self.last_rect.y or \
rect.width != self.last_rect.width or rect.height != self.last_rect.height):
self._pre_calculate_drawing_elements(rect)
return
# Get monitoring state
@@ -159,15 +165,16 @@ class DriverStateRenderer(Widget):
self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None]
# Pre-calculate all drawing elements
self._pre_calculate_drawing_elements()
self._pre_calculate_drawing_elements(rect)
self.state_updated = True
def _pre_calculate_drawing_elements(self):
def _pre_calculate_drawing_elements(self, rect):
"""Pre-calculate all drawing elements based on the current rectangle"""
# Calculate icon position (bottom-left or bottom-right)
width, height = self._rect.width, self._rect.height
width, height = rect.width, rect.height
offset = UI_BORDER_SIZE + BTN_SIZE // 2
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
self.position_y = self._rect.y + height - offset
self.position_x = rect.x + (width - offset if self.is_rhd else offset)
self.position_y = rect.y + height - offset
# Pre-calculate the face lines positions
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
@@ -182,15 +189,15 @@ class DriverStateRenderer(Widget):
# Horizontal arc
h_width = abs(delta_x)
self.h_arc_data = self._calculate_arc_data(
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
)
# Vertical arc
v_height = abs(delta_y)
self.v_arc_data = self._calculate_arc_data(
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
)
def _calculate_arc_data(

View File

@@ -1,78 +0,0 @@
import time
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.widget import Widget
from openpilot.common.params import Params
class ExpButton(Widget):
def __init__(self, button_size: int, icon_size: int):
super().__init__()
self._params = Params()
self._experimental_mode: bool = False
self._engageable: bool = False
# State hold mechanism
self._hold_duration = 2.0 # seconds
self._held_mode: bool | None = None
self._hold_end_time: float | None = None
self._white_color: rl.Color = rl.Color(255, 255, 255, 255)
self._black_bg: rl.Color = rl.Color(0, 0, 0, 166)
self._txt_wheel: rl.Texture = gui_app.texture('icons/chffr_wheel.png', icon_size, icon_size)
self._txt_exp: rl.Texture = gui_app.texture('icons/experimental.png', icon_size, icon_size)
self._rect = rl.Rectangle(0, 0, button_size, button_size)
def set_rect(self, rect: rl.Rectangle) -> None:
self._rect.x, self._rect.y = rect.x, rect.y
def _update_state(self) -> None:
selfdrive_state = ui_state.sm["selfdriveState"]
self._experimental_mode = selfdrive_state.experimentalMode
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled
def handle_mouse_event(self) -> bool:
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect):
if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and
self._is_toggle_allowed()):
new_mode = not self._experimental_mode
self._params.put_bool("ExperimentalMode", new_mode)
# Hold new state temporarily
self._held_mode = new_mode
self._hold_end_time = time.time() + self._hold_duration
return True
return False
def _render(self, rect: rl.Rectangle) -> None:
center_x = int(self._rect.x + self._rect.width // 2)
center_y = int(self._rect.y + self._rect.height // 2)
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect)
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
def _held_or_actual_mode(self):
now = time.time()
if self._hold_end_time and now < self._hold_end_time:
return self._held_mode
if self._hold_end_time and now >= self._hold_end_time:
self._hold_end_time = self._held_mode = None
return self._experimental_mode
def _is_toggle_allowed(self):
if not self._params.get_bool("ExperimentalModeConfirmed"):
return False
car_params = ui_state.sm["carParams"]
if car_params.alphaLongitudinalAvailable:
return self._params.get_bool("AlphaLongitudinalEnabled")
else:
return car_params.openpilotLongitudinalControl

View File

@@ -1,11 +1,10 @@
import pyray as rl
from dataclasses import dataclass
from cereal.messaging import SubMaster
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.common.conversions import Conversions as CV
from openpilot.system.ui.lib.widget import Widget
# Constants
SET_SPEED_NA = 255
@@ -55,25 +54,21 @@ FONT_SIZES = FontSizes()
COLORS = Colors()
class HudRenderer(Widget):
class HudRenderer:
def __init__(self):
super().__init__()
"""Initialize the HUD renderer."""
self.is_cruise_set: bool = False
self.is_cruise_available: bool = False
self.set_speed: float = SET_SPEED_NA
self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False
self._wheel_texture: rl.Texture = gui_app.texture('icons/chffr_wheel.png', UI_CONFIG.wheel_icon_size, UI_CONFIG.wheel_icon_size)
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
def _update_state(self) -> None:
def _update_state(self, sm: SubMaster) -> None:
"""Update HUD state based on car state and controls state."""
sm = ui_state.sm
if sm.recv_frame["carState"] < ui_state.started_frame:
self.is_cruise_set = False
self.set_speed = SET_SPEED_NA
@@ -99,9 +94,9 @@ class HudRenderer(Widget):
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
self.speed = max(0.0, v_ego * speed_conversion)
def _render(self, rect: rl.Rectangle) -> None:
def draw(self, rect: rl.Rectangle, sm: SubMaster) -> None:
"""Render HUD elements to the screen."""
# Draw the header background
self._update_state(sm)
rl.draw_rectangle_gradient_v(
int(rect.x),
int(rect.y),
@@ -115,13 +110,7 @@ class HudRenderer(Widget):
self._draw_set_speed(rect)
self._draw_current_speed(rect)
button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
button_y = rect.y + UI_CONFIG.border_size
self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size))
def handle_mouse_event(self) -> bool:
return bool(self._exp_button.handle_mouse_event())
self._draw_wheel_icon(rect)
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
"""Draw the MAX speed indicator box."""
@@ -177,3 +166,13 @@ class HudRenderer(Widget):
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
def _draw_wheel_icon(self, rect: rl.Rectangle) -> None:
"""Draw the steering wheel icon with status-based opacity."""
center_x = int(rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size / 2)
center_y = int(rect.y + UI_CONFIG.border_size + UI_CONFIG.button_size / 2)
rl.draw_circle(center_x, center_y, UI_CONFIG.button_size / 2, COLORS.black_translucent)
opacity = 0.7 if ui_state.status == UIStatus.DISENGAGED else 1.0
img_pos = rl.Vector2(center_x - self._wheel_texture.width / 2, center_y - self._wheel_texture.height / 2)
rl.draw_texture_v(self._wheel_texture, img_pos, rl.Color(255, 255, 255, int(255 * opacity)))

View File

@@ -7,9 +7,9 @@ from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import DEFAULT_FPS
from openpilot.system.ui.lib.shader_polygon import draw_polygon
from openpilot.system.ui.lib.widget import Widget
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
MAX_DRAW_DISTANCE = 100.0
@@ -36,7 +36,6 @@ class ModelPoints:
raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32))
projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32))
@dataclass
class LeadVehicle:
glow: list[float] = field(default_factory=list)
@@ -44,9 +43,8 @@ class LeadVehicle:
fill_alpha: int = 0
class ModelRenderer(Widget):
class ModelRenderer:
def __init__(self):
super().__init__()
self._longitudinal_control = False
self._experimental_mode = False
self._blend_factor = 1.0
@@ -66,6 +64,11 @@ class ModelRenderer(Widget):
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
self._transform_dirty = True
self._clip_region = None
self._rect = None
# Pre-allocated arrays for polygon conversion
self._temp_points_3d = np.empty((MAX_POINTS * 2, 3), dtype=np.float32)
self._temp_proj = np.empty((3, MAX_POINTS * 2), dtype=np.float32)
self._exp_gradient = {
'start': (0.0, 1.0), # Bottom of path
@@ -83,15 +86,14 @@ class ModelRenderer(Widget):
self._car_space_transform = transform.astype(np.float32)
self._transform_dirty = True
def _render(self, rect: rl.Rectangle):
sm = ui_state.sm
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster):
# Check if data is up-to-date
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
sm.recv_frame["modelV2"] < ui_state.started_frame):
return
# Set up clipping region
self._rect = rect
self._clip_region = rl.Rectangle(
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
)
@@ -125,6 +127,7 @@ class ModelRenderer(Widget):
self._update_leads(radar_state, path_x_array)
self._transform_dirty = False
# Draw elements
self._draw_lane_lines()
self._draw_path(sm)
@@ -253,7 +256,7 @@ class ModelRenderer(Widget):
glow = [(x + (sz * 1.35) + g_xo, y + sz + g_yo), (x, y - g_yo), (x - (sz * 1.35) - g_xo, y + sz + g_yo)]
chevron = [(x + (sz * 1.25), y + sz), (x, y), (x - (sz * 1.25), y + sz)]
return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha))
return LeadVehicle(glow=glow,chevron=chevron, fill_alpha=int(fill_alpha))
def _draw_lane_lines(self):
"""Draw lane lines and road edges"""
@@ -354,60 +357,54 @@ class ModelRenderer(Widget):
if points.shape[0] == 0:
return np.empty((0, 2), dtype=np.float32)
N = points.shape[0]
# Generate left and right 3D points in one array using broadcasting
offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32)
points_3d = points[None, :, :] + offsets[:, None, :] # Shape: 2xNx3
points_3d = points_3d.reshape(2 * N, 3) # Shape: (2*N)x3
# Create left and right 3D points in one array
n_points = points.shape[0]
points_3d = self._temp_points_3d[:n_points * 2]
points_3d[:n_points, 0] = points_3d[n_points:, 0] = points[:, 0]
points_3d[:n_points, 1] = points[:, 1] - y_off
points_3d[n_points:, 1] = points[:, 1] + y_off
points_3d[:n_points, 2] = points_3d[n_points:, 2] = points[:, 2] + z_off
# Transform all points to projected space in one operation
proj = self._car_space_transform @ points_3d.T # Shape: 3x(2*N)
proj = proj.reshape(3, 2, N)
left_proj = proj[:, 0, :]
right_proj = proj[:, 1, :]
# Filter points where z is sufficiently large
valid_proj = (np.abs(left_proj[2]) >= 1e-6) & (np.abs(right_proj[2]) >= 1e-6)
if not np.any(valid_proj):
# Single matrix multiplication for projections
proj = np.ascontiguousarray(self._temp_proj[:, :n_points * 2]) # Slice the pre-allocated array
np.dot(self._car_space_transform, points_3d.T, out=proj)
valid_z = np.abs(proj[2]) > 1e-6
if not np.any(valid_z):
return np.empty((0, 2), dtype=np.float32)
# Compute screen coordinates
left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :]
right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :]
screen = proj[:2, valid_z] / proj[2, valid_z][None, :]
left_screen = screen[:, :n_points].T
right_screen = screen[:, n_points:].T
# Define clip region bounds
clip = self._clip_region
x_min, x_max = clip.x, clip.x + clip.width
y_min, y_max = clip.y, clip.y + clip.height
# Filter points within clip region
left_in_clip = (
(left_screen[0] >= x_min) & (left_screen[0] <= x_max) &
(left_screen[1] >= y_min) & (left_screen[1] <= y_max)
)
right_in_clip = (
(right_screen[0] >= x_min) & (right_screen[0] <= x_max) &
(right_screen[1] >= y_min) & (right_screen[1] <= y_max)
)
both_in_clip = left_in_clip & right_in_clip
if not np.any(both_in_clip):
# Ensure consistent shapes by re-aligning valid points
valid_points = np.minimum(left_screen.shape[0], right_screen.shape[0])
if valid_points == 0:
return np.empty((0, 2), dtype=np.float32)
left_screen = left_screen[:valid_points]
right_screen = right_screen[:valid_points]
# Select valid and clipped points
left_screen = left_screen[:, both_in_clip]
right_screen = right_screen[:, both_in_clip]
# Handle Y-coordinate inversion on hills
if not allow_invert and left_screen.shape[1] > 1:
y = left_screen[1, :] # y-coordinates
keep = y == np.minimum.accumulate(y)
if not np.any(keep):
if self._clip_region:
clip = self._clip_region
bounds_mask = (
(left_screen[:, 0] >= clip.x) & (left_screen[:, 0] <= clip.x + clip.width) &
(left_screen[:, 1] >= clip.y) & (left_screen[:, 1] <= clip.y + clip.height) &
(right_screen[:, 0] >= clip.x) & (right_screen[:, 0] <= clip.x + clip.width) &
(right_screen[:, 1] >= clip.y) & (right_screen[:, 1] <= clip.y + clip.height)
)
if not np.any(bounds_mask):
return np.empty((0, 2), dtype=np.float32)
left_screen = left_screen[:, keep]
right_screen = right_screen[:, keep]
left_screen = left_screen[bounds_mask]
right_screen = right_screen[bounds_mask]
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
if not allow_invert and left_screen.shape[0] > 1:
keep = np.concatenate(([True], np.diff(left_screen[:, 1]) < 0))
left_screen = left_screen[keep]
right_screen = right_screen[keep]
if left_screen.shape[0] == 0:
return np.empty((0, 2), dtype=np.float32)
return np.vstack((left_screen, right_screen[::-1])).astype(np.float32)
@staticmethod
def _map_val(x, x0, x1, y0, y1):
@@ -420,10 +417,10 @@ class ModelRenderer(Widget):
def _hsla_to_color(h, s, l, a):
rgb = colorsys.hls_to_rgb(h, l, s)
return rl.Color(
int(rgb[0] * 255),
int(rgb[1] * 255),
int(rgb[2] * 255),
int(a * 255)
int(rgb[0] * 255),
int(rgb[1] * 255),
int(rgb[2] * 255),
int(a * 255)
)
@staticmethod

View File

@@ -45,6 +45,17 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
});
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
is_release = params.getBool("IsReleaseBranch");
@@ -93,6 +104,8 @@ void DeveloperPanel::updateToggles(bool _offroad) {
experimentalLongitudinalToggle->refresh();
// Handle specific controls visibility for release branches
enableGithubRunner->setVisible(!is_release);
errorLogBtn->setVisible(!is_release);
joystickToggle->setVisible(!is_release);
offroad = _offroad;

View File

@@ -16,8 +16,10 @@ private:
Params params;
ParamControl* adbToggle;
ParamControl* joystickToggle;
ButtonControl* errorLogBtn;
ParamControl* longManeuverToggle;
ParamControl* experimentalLongitudinalToggle;
ParamControl* enableGithubRunner;
bool is_release;
bool offroad = false;

View File

@@ -22,7 +22,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent)
layout->setSpacing(20);
// header
QLabel *title = new QLabel(tr("Firehose Mode"));
QLabel *title = new QLabel(tr("🔥 Firehose Mode 🔥"));
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';");
layout->addWidget(title, 0, Qt::AlignCenter);

View File

@@ -68,13 +68,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
"../assets/icons/monitoring.png",
true,
},
{
"RecordAudio",
tr("Record and Upload Microphone Audio"),
tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
"../assets/icons/microphone.png",
true,
},
{
"IsMetric",
tr("Use Metric System"),
@@ -146,15 +139,6 @@ void TogglesPanel::expandToggleDescription(const QString &param) {
toggles[param.toStdString()]->showDescription();
}
void TogglesPanel::scrollToToggle(const QString &param) {
if (auto it = toggles.find(param.toStdString()); it != toggles.end()) {
auto scroll_area = qobject_cast<QScrollArea*>(parent()->parent());
if (scroll_area) {
scroll_area->ensureWidgetVisible(it->second);
}
}
}
void TogglesPanel::showEvent(QShowEvent *event) {
updateToggles();
}
@@ -238,7 +222,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
addItem(dcamBtn);
#endif
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
if (!uiState()->engaged()) {
@@ -251,7 +235,6 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
params.remove("LiveParametersV2");
params.remove("LiveDelay");
params.putBool("OnroadCycleRequested", true);
updateCalibDescription();
}
}
} else {
@@ -330,7 +313,9 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
}
void DevicePanel::updateCalibDescription() {
QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down.");
QString desc =
tr("sunnypilot requires the device to be mounted within 4° left or right and "
"within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required.");
std::string calib_bytes = params.get("CalibrationParams");
if (!calib_bytes.empty()) {
try {
@@ -348,48 +333,8 @@ void DevicePanel::updateCalibDescription() {
qInfo() << "invalid CalibrationParams";
}
}
int lag_perc = 0;
std::string lag_bytes = params.get("LiveDelay");
if (!lag_bytes.empty()) {
try {
AlignedBuffer aligned_buf;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size()));
lag_perc = cmsg.getRoot<cereal::Event>().getLiveDelay().getCalPerc();
} catch (kj::Exception) {
qInfo() << "invalid LiveDelay";
}
}
if (lag_perc < 100) {
desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc);
} else {
desc += tr("\n\nSteering lag calibration is complete.");
}
std::string torque_bytes = params.get("LiveTorqueParameters");
if (!torque_bytes.empty()) {
try {
AlignedBuffer aligned_buf;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size()));
auto torque = cmsg.getRoot<cereal::Event>().getLiveTorqueParameters();
// don't add for non-torque cars
if (torque.getUseParams()) {
int torque_perc = torque.getCalPerc();
if (torque_perc < 100) {
desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc);
} else {
desc += tr(" Steering torque response calibration is complete.");
}
}
} catch (kj::Exception) {
qInfo() << "invalid LiveTorqueParameters";
}
}
desc += "\n\n";
desc += tr("openpilot is continuously calibrating, resetting is rarely required. "
"Resetting calibration will restart openpilot if the car is powered on.");
resetCalibBtn->setDescription(desc);
desc += tr(" Resetting calibration will restart openpilot if the car is powered on.");
qobject_cast<ButtonControl *>(sender())->setDescription(desc);
}
void DevicePanel::reboot() {
@@ -442,7 +387,6 @@ void SettingsWindow::setCurrentPanel(int index, const QString &param) {
}
} else {
emit expandToggleDescription(param);
emit scrollToToggle(param);
}
}
@@ -483,7 +427,6 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
TogglesPanel *toggles = new TogglesPanel(this);
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
auto networking = new Networking(this);
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);

View File

@@ -42,7 +42,6 @@ signals:
void reviewTrainingGuide();
void showDriverView();
void expandToggleDescription(const QString &param);
void scrollToToggle(const QString &param);
protected:
QPushButton *sidebar_alert_widget;
@@ -68,7 +67,6 @@ protected slots:
protected:
Params params;
ButtonControl *pair_device;
ButtonControl *resetCalibBtn;
};
class TogglesPanel : public ListWidget {
@@ -79,7 +77,6 @@ public:
public slots:
void expandToggleDescription(const QString &param);
void scrollToToggle(const QString &param);
protected slots:
virtual void updateState(const UIState &s);

View File

@@ -34,7 +34,6 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
drawLead(painter, lead_two, lead_vertices[1], surface_rect);
}
}
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
painter.restore();
}
@@ -174,173 +173,6 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
(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,
const QPointF &vd, const QRect &surface_rect) {
const float speedBuff = 10.;

View File

@@ -34,12 +34,6 @@ protected:
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
QPolygonF *pvd, int max_idx, bool allow_invert = true);
void 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 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);
@@ -64,9 +58,4 @@ protected:
QPointF lead_vertices[2] = {};
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
QRectF clip_region;
float lead_status_alpha = 0.0f;
QPointF lead_status_pos;
QString lead_status_text;
QColor lead_status_color;
};

View File

@@ -136,59 +136,6 @@ QWidget * Setup::low_voltage() {
return widget;
}
QWidget * Setup::custom_software_warning() {
QWidget *widget = new QWidget();
QVBoxLayout *main_layout = new QVBoxLayout(widget);
main_layout->setContentsMargins(55, 0, 55, 55);
main_layout->setSpacing(0);
QVBoxLayout *inner_layout = new QVBoxLayout();
inner_layout->setContentsMargins(110, 110, 300, 0);
main_layout->addLayout(inner_layout);
QLabel *title = new QLabel(tr("WARNING: Custom Software"));
title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;");
inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
inner_layout->addSpacing(25);
QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later."));
body->setWordWrap(true);
body->setAlignment(Qt::AlignTop | Qt::AlignLeft);
body->setStyleSheet("font-size: 65px; font-weight: 300;");
inner_layout->addWidget(body);
inner_layout->addStretch();
QHBoxLayout *blayout = new QHBoxLayout();
blayout->setSpacing(50);
main_layout->addLayout(blayout, 0);
QPushButton *back = new QPushButton(tr("Back"));
back->setObjectName("navBtn");
blayout->addWidget(back);
QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage);
QPushButton *cont = new QPushButton(tr("Continue"));
cont->setObjectName("navBtn");
blayout->addWidget(cont);
QObject::connect(cont, &QPushButton::clicked, this, [=]() {
QTimer::singleShot(0, [=]() {
setCurrentWidget(downloading_widget);
});
QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
if (!url.isEmpty()) {
QTimer::singleShot(1000, this, [=]() {
download(url);
});
} else {
setCurrentWidget(software_selection_widget);
}
});
return widget;
}
QWidget * Setup::getting_started() {
QWidget *widget = new QWidget();
@@ -358,17 +305,20 @@ QWidget * Setup::software_selection() {
blayout->addWidget(cont);
QObject::connect(cont, &QPushButton::clicked, [=]() {
auto w = currentWidget();
QTimer::singleShot(0, [=]() {
setCurrentWidget(downloading_widget);
});
QString url = OPENPILOT_URL;
if (group->checkedButton() != openpilot) {
QTimer::singleShot(0, [=]() {
setCurrentWidget(custom_software_warning_widget);
url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
}
if (!url.isEmpty()) {
QTimer::singleShot(1000, this, [=]() {
download(url);
});
} else {
QTimer::singleShot(0, [=]() {
setCurrentWidget(downloading_widget);
});
QTimer::singleShot(1000, this, [=]() {
download(OPENPILOT_URL);
});
setCurrentWidget(w);
}
});
@@ -465,10 +415,8 @@ Setup::Setup(QWidget *parent) : QStackedWidget(parent) {
addWidget(getting_started());
addWidget(network_setup());
software_selection_widget = software_selection();
addWidget(software_selection_widget);
custom_software_warning_widget = custom_software_warning();
addWidget(custom_software_warning_widget);
addWidget(software_selection());
downloading_widget = downloading();
addWidget(downloading_widget);

View File

@@ -15,7 +15,6 @@ public:
private:
void selectLanguage();
QWidget *low_voltage();
QWidget *custom_software_warning();
QWidget *getting_started();
QWidget *network_setup();
QWidget *software_selection();
@@ -24,8 +23,6 @@ private:
QWidget *failed_widget;
QWidget *downloading_widget;
QWidget *custom_software_warning_widget;
QWidget *software_selection_widget;
QTranslator translator;
signals:

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);
}
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());
flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size());
settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio);
mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30));
connect(this, &Sidebar::valueChanged, [=] { update(); });
@@ -48,15 +47,12 @@ void Sidebar::mousePressEvent(QMouseEvent *event) {
} else if (settings_btn.contains(event->pos())) {
settings_pressed = true;
update();
} else if (recording_audio && mic_indicator_btn.contains(event->pos())) {
mic_indicator_pressed = true;
update();
}
}
void Sidebar::mouseReleaseEvent(QMouseEvent *event) {
if (flag_pressed || settings_pressed || mic_indicator_pressed) {
flag_pressed = settings_pressed = mic_indicator_pressed = false;
if (flag_pressed || settings_pressed) {
flag_pressed = settings_pressed = false;
update();
}
if (onroad && home_btn.contains(event->pos())) {
@@ -65,8 +61,6 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) {
pm->send("userFlag", msg);
} else if (settings_btn.contains(event->pos())) {
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};
}
setProperty("pandaStatus", QVariant::fromValue(pandaStatus));
setProperty("recordingAudio", s.scene.recording_audio);
}
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.setOpacity(onroad && flag_pressed ? 0.65 : 1.0);
p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img);
if (recording_audio) {
p.setBrush(danger_color);
p.setOpacity(mic_indicator_pressed ? 0.65 : 1.0);
p.drawRoundedRect(mic_indicator_btn, mic_indicator_btn.height() / 2, mic_indicator_btn.height() / 2);
int icon_x = mic_indicator_btn.x() + (mic_indicator_btn.width() - mic_img.width()) / 2;
int icon_y = mic_indicator_btn.y() + (mic_indicator_btn.height() - mic_img.height()) / 2;
p.drawPixmap(icon_x, icon_y, mic_img);
}
p.setOpacity(1.0);
// network

View File

@@ -23,7 +23,6 @@ class Sidebar : public QFrame {
Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged);
Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged);
Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged);
Q_PROPERTY(bool recordingAudio MEMBER recording_audio NOTIFY valueChanged);
public:
explicit Sidebar(QWidget* parent = 0);
@@ -43,8 +42,8 @@ protected:
void drawMetric(QPainter &p, const QPair<QString, QString> &label, QColor c, int y);
virtual void drawSidebar(QPainter &p);
QPixmap home_img, flag_img, settings_img, mic_img;
bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed;
QPixmap home_img, flag_img, settings_img;
bool onroad, flag_pressed, settings_pressed;
const QMap<cereal::DeviceState::NetworkType, QString> network_type = {
{cereal::DeviceState::NetworkType::NONE, tr("--")},
{cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")},
@@ -57,7 +56,6 @@ protected:
const QRect home_btn = QRect(60, 860, 180, 180);
const QRect settings_btn = QRect(50, 35, 200, 117);
const QRect mic_indicator_btn = QRect(158, 252, 75, 40);
const QColor good_color = QColor(255, 255, 255);
const QColor warning_color = QColor(218, 202, 37);
const QColor danger_color = QColor(201, 34, 49);

View File

@@ -2,7 +2,6 @@
#include <QPushButton>
#include <QButtonGroup>
#include <QScroller>
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h"
@@ -335,141 +334,3 @@ QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStrin
}
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 "";
}

View File

@@ -6,7 +6,6 @@
#include <QString>
#include <QVBoxLayout>
#include <QWidget>
#include <QTreeWidget>
#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);
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;
};

View File

@@ -139,7 +139,7 @@ class Soundd(QuietMode):
# sounddevice must be imported after forking processes
import sounddevice as sd
sm = messaging.SubMaster(['selfdriveState', 'soundPressure'])
sm = messaging.SubMaster(['selfdriveState', 'microphone'])
with self.get_stream(sd) as stream:
rk = Ratekeeper(20)
@@ -150,8 +150,8 @@ class Soundd(QuietMode):
self.load_param()
if sm.updated['soundPressure'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb)
if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb)
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
self.get_audible_alert(sm)

View File

@@ -21,7 +21,6 @@ qt_src = [
"sunnypilot/qt/home.cc",
"sunnypilot/qt/offroad/exit_offroad_button.cc",
"sunnypilot/qt/offroad/offroad_home.cc",
"sunnypilot/qt/offroad/settings/developer_panel.cc",
"sunnypilot/qt/offroad/settings/device_panel.cc",
"sunnypilot/qt/offroad/settings/lateral_panel.cc",
"sunnypilot/qt/offroad/settings/longitudinal_panel.cc",
@@ -47,7 +46,6 @@ lateral_panel_qt_src = [
"sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/mads_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/angle_tuning_settings.cc",
"sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc",
]

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -87,17 +87,6 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
params.put("DeviceBootMode", QString::number(index).toStdString());
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 = new Brightness();
@@ -209,11 +198,4 @@ void DevicePanelSP::updateState() {
currStatus = DeviceSleepModeStatus::OFFROAD;
}
toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus));
QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout"));
if (timeoutValue == "0") {
interactivityTimeout->setLabel("DEFAULT");
} else {
interactivityTimeout->setLabel(timeoutValue + "s");
}
}

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