16 Commits

Author SHA1 Message Date
mawei 317d51566b fix: set jihulab.com as update source for pre branch 2026-06-01 11:25:19 +08:00
mawei d606f7d723 fix: delay relay switching 5s on MQB startup to prevent vehicle errors 2026-06-01 11:01:44 +08:00
Rick Lan 1986516b32 pre-build 2026-02-27 10:31:23 +08:00
Rick Lan 6f274a1e59 translations - dragonpilot 2026-02-27 10:30:44 +08:00
Rick Lan e889c3bf19 translations - app 2026-02-27 10:30:44 +08:00
Rick Lan 3346e4d281 [bugfix] sdsu dist btn 2026-02-27 10:30:08 +08:00
Rick Lan 80ac65a3e2 [bugfix] skip audio stream when OSError 2026-02-25 15:54:07 +08:00
Rick Lan e5ffea4734 [Temp] Adjust DASR 2026-02-20 20:45:19 +08:00
Rick Lan 3ef072024f Merge branch 'brand/honda/nidec-stock-long' into pre-patch 2026-02-20 20:43:54 +08:00
Rick Lan b5c149dfed brand/honda/nidec-stock-long
2026-02-20: init
2026-02-20 20:30:53 +08:00
Rick Lan bdf701af10 better looking fonts 2026-02-13 16:42:23 +08:00
Rick Lan 0634122d32 [Fix] VW PQ: Revert change to gear position VAL table (#3020) 2026-02-13 16:42:23 +08:00
Rick Lan 5b95afd3dc feat: Squash all pre-brand features into pre 2026-02-13 16:41:56 +08:00
Rick Lan 01e9d51093 feat: Squash all min-features into full 2026-02-13 16:14:14 +08:00
Rick Lan 0076d30d62 feat: Squash all core features into min 2026-01-23 11:48:04 +08:00
Vehicle Researcher 6928314c89 openpilot v0.10.3 release
date: 2025-12-18T23:23:16
master commit: 154c2334110373950bac1c36fc6e943cb1208326
2025-12-18 23:23:21 -08:00
3714 changed files with 354265 additions and 755246 deletions
-58
View File
@@ -1,58 +0,0 @@
name: 'automatically cache based on current runner'
inputs:
path:
description: 'path to cache'
required: true
key:
description: 'key'
required: true
restore-keys:
description: 'restore-keys'
required: true
save:
description: 'whether to save the cache'
default: 'true'
required: false
outputs:
cache-hit:
description: 'cache hit occurred'
value: ${{ (contains(runner.name, 'nsc') && steps.ns-cache.outputs.cache-hit) ||
(!contains(runner.name, 'nsc') && inputs.save != 'false' && steps.gha-cache.outputs.cache-hit) ||
(!contains(runner.name, 'nsc') && inputs.save == 'false' && steps.gha-cache-ro.outputs.cache-hit) }}
runs:
using: "composite"
steps:
- name: setup namespace cache
id: ns-cache
if: ${{ contains(runner.name, 'nsc') }}
uses: namespacelabs/nscloud-cache-action@v1
with:
path: ${{ inputs.path }}
- name: setup github cache
id: gha-cache
if: ${{ !contains(runner.name, 'nsc') && inputs.save != 'false' }}
uses: 'actions/cache@v4'
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
restore-keys: ${{ inputs.restore-keys }}
- name: setup github cache
id: gha-cache-ro
if: ${{ !contains(runner.name, 'nsc') && inputs.save == 'false' }}
uses: 'actions/cache/restore@v4'
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
restore-keys: ${{ inputs.restore-keys }}
# make the directory manually in case we didn't get a hit, so it doesn't fail on future steps
- id: scons-cache-setup
shell: bash
run: |
mkdir -p ${{ inputs.path }}
sudo chmod -R 777 ${{ inputs.path }}
sudo chown -R $USER ${{ inputs.path }}
-52
View File
@@ -1,52 +0,0 @@
name: "PR review"
on:
pull_request_target:
types: [opened, reopened, synchronize, edited]
jobs:
labeler:
name: review
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: false
# Label PRs
- uses: actions/labeler@v5.0.0
with:
dot: true
configuration-path: .github/labeler.yaml
# Check PR target branch
- name: check branch
uses: Vankka/pr-target-branch-action@def32ec9d93514138d6ac0132ee62e120a72aed5
if: github.repository == 'commaai/openpilot'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target: /^(?!master$).*/
exclude: /commaai:.*/
change-to: ${{ github.base_ref }}
already-exists-action: close_this
already-exists-comment: "Your PR should be made against the `master` branch"
# Welcome comment
- name: "First timers PR"
uses: actions/first-interaction@v1
if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot'
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |
<!-- _(run_id **${{ github.run_id }}**)_ -->
Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following:
* Convert your PR to a draft unless it's ready to review
* Read the [contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md)
* Before marking as "ready for review", ensure:
* the goal is clearly stated in the description
* all the tests are passing
* the change is [something we merge](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged)
* include a route or your device' dongle ID if relevant
-37
View File
@@ -1,37 +0,0 @@
name: badges
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
env:
BASE_IMAGE: openpilot-base
DOCKER_REGISTRY: ghcr.io/commaai
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $DOCKER_REGISTRY/$BASE_IMAGE:latest /bin/bash -c
jobs:
badges:
name: create badges
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Push badges
run: |
${{ env.RUN }} "python3 selfdrive/ui/translations/create_badges.py"
rm .gitattributes
git checkout --orphan badges
git rm -rf --cached .
git config user.email "badge-researcher@comma.ai"
git config user.name "Badge Researcher"
git add translation_badge.svg
git commit -m "Add/Update badges"
git push -f origin HEAD
-101
View File
@@ -1,101 +0,0 @@
name: weekly CI test report
on:
schedule:
- cron: '37 9 * * 1' # 9:37AM UTC -> 2:37AM PST every monday
workflow_dispatch:
inputs:
ci_runs:
description: 'The amount of runs to trigger in CI test report'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CI_RUNS: ${{ github.event.inputs.ci_runs || '50' }}
jobs:
setup:
if: github.repository == 'commaai/openpilot'
runs-on: ubuntu-latest
outputs:
ci_runs: ${{ steps.ci_runs_setup.outputs.matrix }}
steps:
- id: ci_runs_setup
name: CI_RUNS=${{ env.CI_RUNS }}
run: |
matrix=$(python3 -c "import json; print(json.dumps({ 'run_number' : list(range(${{ env.CI_RUNS }})) }))")
echo "matrix=$matrix" >> $GITHUB_OUTPUT
ci_matrix_run:
needs: [ setup ]
strategy:
fail-fast: false
matrix: ${{fromJSON(needs.setup.outputs.ci_runs)}}
uses: commaai/openpilot/.github/workflows/ci_weekly_run.yaml@master
with:
run_number: ${{ matrix.run_number }}
report:
needs: [ci_matrix_run]
runs-on: ubuntu-latest
if: always() && github.repository == 'commaai/openpilot'
steps:
- name: Get job results
uses: actions/github-script@v7
id: get-job-results
with:
script: |
const jobs = await github
.paginate("GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt}/jobs", {
owner: "commaai",
repo: "${{ github.event.repository.name }}",
run_id: "${{ github.run_id }}",
attempt: "${{ github.run_attempt }}",
})
var report = {}
jobs.slice(1, jobs.length-1).forEach(job => {
if (job.conclusion === "skipped") return;
const jobName = job.name.split(" / ")[2];
const runRegex = /\((.*?)\)/;
const run = job.name.match(runRegex)[1];
report[jobName] = report[jobName] || { successes: [], failures: [], canceled: [] };
switch (job.conclusion) {
case "success":
report[jobName].successes.push({ "run_number": run, "link": job.html_url}); break;
case "failure":
report[jobName].failures.push({ "run_number": run, "link": job.html_url }); break;
case "canceled":
report[jobName].canceled.push({ "run_number": run, "link": job.html_url }); break;
}
});
return JSON.stringify({"jobs": report});
- name: Add job results to summary
env:
JOB_RESULTS: ${{ fromJSON(steps.get-job-results.outputs.result) }}
run: |
cat <<EOF >> template.html
<table>
<thead>
<tr>
<th></th>
<th>Job</th>
<th>✅ Passing</th>
<th>❌ Failure Details</th>
</tr>
</thead>
<tbody>
{% for key in jobs.keys() %}<tr>
<td>{% for i in range(5) %}{% if i+1 <= (5 * jobs[key]["successes"]|length // ${{ env.CI_RUNS }}) %}🟩{% else %}🟥{% endif %}{% endfor%}</td>
<td>{{ key }}</td>
<td>{{ 100 * jobs[key]["successes"]|length // ${{ env.CI_RUNS }} }}%</td>
<td>{% if jobs[key]["failures"]|length > 0 %}<details>{% for failure in jobs[key]["failures"] %}<a href="{{ failure['link'] }}">Log for run #{{ failure['run_number'] }}</a><br>{% endfor %}</details>{% else %}{% endif %}</td>
</td>
</tr>{% endfor %}
</table>
EOF
pip install jinja2-cli
echo $JOB_RESULTS | jinja2 template.html > report.html
echo "# CI Test Report - ${{ env.CI_RUNS }} Runs" >> $GITHUB_STEP_SUMMARY
cat report.html >> $GITHUB_STEP_SUMMARY
-17
View File
@@ -1,17 +0,0 @@
name: weekly CI test run
on:
workflow_call:
inputs:
run_number:
required: true
type: string
concurrency:
group: ci-run-${{ inputs.run_number }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
uses: commaai/openpilot/.github/workflows/tests.yaml@master
with:
run_number: ${{ inputs.run_number }}
@@ -1,21 +0,0 @@
name: 'compile openpilot'
runs:
using: "composite"
steps:
- shell: bash
name: Build openpilot with all flags
run: |
${{ env.RUN }} "scons -j$(nproc)"
${{ env.RUN }} "release/check-dirty.sh"
- shell: bash
name: Cleanup scons cache and rebuild
run: |
${{ env.RUN }} "rm -rf /tmp/scons_cache/* && \
scons -j$(nproc) --cache-populate"
- name: Save scons cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master'
with:
path: .ci_cache/scons_cache
key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
-239
View File
@@ -1,239 +0,0 @@
name: Compile StarPilot
on:
workflow_dispatch:
inputs:
not_vetted:
description: "This branch is not vetted"
type: boolean
default: false
required: false
publish_custom_branch:
description: "Push to custom branch:"
type: string
default: ""
required: false
publish_starpilot:
description: "Push to StarPilot"
type: boolean
default: false
required: false
publish_staging:
description: "Push to StarPilot-Staging"
type: boolean
default: false
required: false
publish_testing:
description: "Push to StarPilot-Testing"
type: boolean
default: false
required: false
runner:
description: "Select runner"
type: choice
options:
- c3
- c3x
default: "c3"
required: true
update_translations:
description: "Update missing/outdated translations"
type: boolean
default: false
required: false
vet_existing_translations:
description: "Vet existing translations"
type: boolean
default: false
required: false
env:
BASE_DIR: ${{ github.workspace }}
BUILD_DIR: "/data/openpilot"
CUSTOM_BRANCH: ${{ inputs.publish_custom_branch }}
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
jobs:
get_branch:
runs-on: [self-hosted, "${{ inputs.runner }}"]
outputs:
branch: ${{ steps.get_branch.outputs.branch }}
python_version: ${{ steps.get_python_version.outputs.python_version }}
steps:
- name: Get Current Branch
id: get_branch
run: |
cd "$BUILD_DIR"
echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> "$GITHUB_OUTPUT"
- name: Get Python Version
id: get_python_version
run: |
echo "python_version=$(tr -d '[:space:]' < "$BUILD_DIR/.python-version")" >> $GITHUB_OUTPUT
translate:
needs: get_branch
if: inputs.update_translations
runs-on: ubuntu-latest
steps:
- name: Configure Git Identity
run: |
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
- name: Checkout Required Files
uses: actions/checkout@v4
with:
ref: ${{ needs.get_branch.outputs.branch }}
sparse-checkout: |
starpilot/ui/
selfdrive/controls/lib/alerts_offroad.json
selfdrive/ui/
selfdrive/ui/translations/
selfdrive/ui/translations/auto_translate.py
selfdrive/ui/update_translations.py
- name: Set Up Python
uses: actions/setup-python@v4
with:
cache: "pip"
python-version: ${{ needs.get_branch.outputs.python_version }}
- name: Install Dependencies
run: |
pip install requests
sudo apt-get update && sudo apt-get install -y --no-install-recommends qttools5-dev-tools
- name: Update Translations
run: |
python selfdrive/ui/update_translations.py --vanish
- name: Update Missing Translations
continue-on-error: true
timeout-minutes: 300
run: |
python selfdrive/ui/translations/auto_translate.py --all-files
- name: Vet Existing Translations
if: inputs.vet_existing_translations
continue-on-error: true
timeout-minutes: 300
run: |
python selfdrive/ui/translations/auto_translate.py --all-files --vet-translations
- name: Commit and Push Translations
run: |
if git diff --quiet selfdrive/ui/translations/*.ts; then
echo "No translation updates detected."
exit 0
fi
git fetch --unshallow origin "${{ needs.get_branch.outputs.branch }}"
git checkout "${{ needs.get_branch.outputs.branch }}"
git add selfdrive/ui/translations/*.ts
git commit --amend --no-edit
git push --force origin "${{ needs.get_branch.outputs.branch }}"
build_and_push:
needs: [get_branch, translate]
if: ${{ !failure() && !cancelled() && needs.get_branch.result == 'success' }}
runs-on: [self-hosted, "${{ inputs.runner }}"]
permissions:
contents: write
defaults:
run:
working-directory: ${{ env.BUILD_DIR }}
steps:
- name: Configure Git
run: |
git config http.postBuffer 104857600
git config user.name "$GIT_NAME"
git config user.email "$GIT_EMAIL"
git remote set-url origin "https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/FrogAi/StarPilot.git"
- name: Sync Translation Updates
if: inputs.update_translations
run: |
git fetch origin "${{ needs.get_branch.outputs.branch }}"
git reset --hard FETCH_HEAD
- name: Take Ownership of Build Directory
run: |
sudo chown -R $(whoami):$(whoami) .
- name: Clean Build Artifacts
run: |
find . -name "matlab.*.md" -delete
find . -type d \( -iname "debug" -o -iname "test" -o -iname "tests" -o -name '__pycache__' \) -exec rm -rf {} +
find . -type f \( \
-name '*.a' -o \
-name '*.o' -o \
-name '*.onnx' -o \
-name '*.os' -o \
-name '*.pyc' -o \
-name 'moc_*' \
\) -delete
find .github -mindepth 1 -maxdepth 1 ! -name 'workflows' -exec rm -rf {} +
find .github/workflows -mindepth 1 ! \( \
-type f \( \
-name 'compile_starpilot.yaml' -o \
-name 'review_pull_request.yaml' -o \
-name 'schedule_update.yaml' -o \
-name 'update_pr_branch.yaml' -o \
-name 'update_release_branch.yaml' -o \
-name 'update_tinygrad.yaml' \
\) \
\) -exec rm -rf {} +
find panda/board -type f \
! -name '__init__.py' \
! -name 'bootstub.panda.bin' \
! -name 'bootstub.panda_h7.bin' \
! -name 'panda.bin.signed' \
! -name 'panda_h7.bin.signed' \
-delete
find third_party/ -name '*Darwin*' -exec rm -rf {} +
find third_party/ -name '*x86*' -exec rm -rf {} +
rm -f .gitignore .gitmodules .gitattributes .lfsconfig .overlay_init
rm -rf .sconsign.dblite .vscode/ Jenkinsfile release/ scripts/ site_scons/ teleoprtc_repo/
find . -type d -empty ! -path "./.git*" -delete
touch prebuilt
[ "${{ inputs.not_vetted }}" = "true" ] && touch not_vetted || true
- name: Add Update Date File
if: inputs.publish_staging
continue-on-error: true
run: |
curl -fLsS https://raw.githubusercontent.com/FrogAi/StarPilot/StarPilot-Staging/.github/update_date -o .github/update_date || echo "No update_date found, skipping..."
- name: Commit and Push Build
run: |
git add -f .
git commit -m "Compile StarPilot"
git push --force origin HEAD
if [ "${{ inputs.publish_starpilot }}" = "true" ]; then
git push --force origin HEAD:StarPilot
fi
if [ "${{ inputs.publish_staging }}" = "true" ]; then
git push --force origin HEAD:StarPilot-Staging
fi
if [ "${{ inputs.publish_testing }}" = "true" ]; then
git push --force origin HEAD:StarPilot-Testing
fi
if [ -n "$CUSTOM_BRANCH" ]; then
git push --force origin HEAD:"$CUSTOM_BRANCH"
fi
-65
View File
@@ -1,65 +0,0 @@
name: docs
on:
push:
branches:
- master
pull_request:
workflow_call:
inputs:
run_number:
default: '1'
required: true
type: string
concurrency:
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
docs:
name: build docs
runs-on: ubuntu-24.04
steps:
- uses: commaai/timeout@v1
- uses: actions/checkout@v4
with:
submodules: true
# Build
- name: Build docs
run: |
# TODO: can we install just the "docs" dependency group without the normal deps?
pip install mkdocs
mkdocs build
# Push to docs.comma.ai
- uses: actions/checkout@v4
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot'
with:
path: openpilot-docs
ssh-key: ${{ secrets.OPENPILOT_DOCS_KEY }}
repository: commaai/openpilot-docs
- name: Push
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot'
run: |
set -x
source release/identity.sh
cd openpilot-docs
git checkout --orphan tmp
git rm -rf .
# copy over docs
cp -r ../docs_site/ docs/
# GitHub pages config
touch docs/.nojekyll
echo -n docs.comma.ai > docs/CNAME
git add -f .
git commit -m "build docs"
# docs live in different repo to not bloat openpilot's full clone size
git push -f origin tmp:gh-pages
-59
View File
@@ -1,59 +0,0 @@
name: jenkins scan
on:
issue_comment:
types: [created, edited]
jobs:
# TODO: gc old branches in a separate job in this workflow
scan-comments:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request }}
permissions:
contents: write
issues: write
steps:
- name: Check for trigger phrase
id: check_comment
uses: actions/github-script@v7
with:
script: |
const triggerPhrase = "trigger-jenkins";
const comment = context.payload.comment.body;
const commenter = context.payload.comment.user.login;
const { data: permissions } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter
});
const hasWriteAccess = permissions.permission === 'write' || permissions.permission === 'admin';
return (hasWriteAccess && comment.includes(triggerPhrase));
result-encoding: json
- name: Checkout repository
if: steps.check_comment.outputs.result == 'true'
uses: actions/checkout@v4
with:
ref: refs/pull/${{ github.event.issue.number }}/head
- name: Push to tmp-jenkins branch
if: steps.check_comment.outputs.result == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b tmp-jenkins-${{ github.event.issue.number }}
GIT_LFS_SKIP_PUSH=1 git push -f origin tmp-jenkins-${{ github.event.issue.number }}
- name: Delete trigger comment
if: steps.check_comment.outputs.result == 'true' && always()
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
});
@@ -1,151 +0,0 @@
name: "mici raylib ui preview"
on:
push:
branches:
- master
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]
branches:
- 'master'
paths:
- 'selfdrive/assets/**'
- 'selfdrive/ui/**'
- 'system/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create mici raylib UI Report"
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-mici-raylib-ui"
MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib"
# All report files are pushed here
REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports"
jobs:
preview:
if: github.repository == 'commaai/openpilot'
name: preview
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
pull-requests: write
actions: read
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Waiting for ui generation to end
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ env.SHA }}
check-name: ${{ env.UI_JOB_NAME }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
allowed-conclusions: success
wait-interval: 20
- name: Getting workflow run ID
id: get_run_id
run: |
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT
- name: Getting proposed ui # filename: pr_ui/mici_ui_replay.mp4
id: download-artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
run_id: ${{ steps.get_run_id.outputs.run_id }}
search_artifacts: true
name: mici-raylib-report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4
uses: actions/checkout@v4
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/master_ui_raylib
ref: ${{ env.MASTER_BRANCH_NAME }}
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git checkout --orphan=new_master_ui_mici_raylib
git rm -rf *
git branch -D ${{ env.MASTER_BRANCH_NAME }}
git branch -m ${{ env.MASTER_BRANCH_NAME }}
git config user.name "GitHub Actions Bot"
git config user.email "<>"
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "mici raylib video for commit ${{ env.SHA }}"
git push origin ${{ env.MASTER_BRANCH_NAME }} --force
- name: Setup FFmpeg
uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae
- name: Finding diff
if: github.event_name == 'pull_request_target'
id: find_diff
run: |
# Find the video file from PR
pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4"
mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video"
master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4"
mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video"
# Run report
export PYTHONPATH=${{ github.workspace }}
baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}"
diff_exit_code=0
python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$?
# Copy diff report files
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/
REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html"
if [ $diff_exit_code -eq 0 ]; then
DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)"
else
DIFF="❌ <strong>Videos differ!</strong> [View Diff Report]($REPORT_URL)"
fi
echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT"
- name: Saving proposed ui
if: github.event_name == 'pull_request_target'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
# Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference
git config user.name "GitHub Actions Bot"
git config user.email "<>"
git checkout --orphan=${{ env.BRANCH_NAME }}
git rm -rf *
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "mici raylib video for PR #${{ github.event.number }}"
git push origin ${{ env.BRANCH_NAME }} --force
# Append diff report to report files branch
git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }}
git checkout ${{ env.REPORT_FILES_BRANCH_NAME }}
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html
git add diff_pr_${{ github.event.number }}.html
git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit"
git push origin ${{ env.REPORT_FILES_BRANCH_NAME }}
- name: Comment Video on PR
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
<!-- _(run_id_video_mici_raylib **${{ github.run_id }}**)_ -->
## mici raylib UI Preview
${{ steps.find_diff.outputs.DIFF }}
comment_tag: run_id_video_mici_raylib
pr_number: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-42
View File
@@ -1,42 +0,0 @@
name: "model review"
on:
pull_request:
types: [opened, reopened, synchronize]
paths:
- 'selfdrive/modeld/models/*.onnx'
workflow_dispatch:
jobs:
comment:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout master
uses: actions/checkout@v4
with:
ref: master
path: base
- run: git lfs pull
- run: cd base && git lfs pull
- run: pip install onnx
- name: scripts/reporter.py
id: report
run: |
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "## Model Review" >> $GITHUB_OUTPUT
MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Post model report comment
uses: marocchino/sticky-pull-request-comment@baa7203ed60924babbe5dcd0ac8eae3b66ec5e16
with:
header: model-review
message: ${{ steps.report.outputs.content }}
-39
View File
@@ -1,39 +0,0 @@
name: prebuilt
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
env:
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: selfdrive/test/docker_build.sh prebuilt
jobs:
build_prebuilt:
name: build prebuilt
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
env:
PUSH_IMAGE: true
permissions:
checks: read
contents: read
packages: write
steps:
- name: Wait for green check mark
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master
wait-interval: 30
running-workflow-name: 'build prebuilt'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build master-ci).*).)*$
- uses: actions/checkout@v4
with:
submodules: true
- run: git lfs pull
- name: Build and Push docker image
run: |
$DOCKER_LOGIN
eval "$BUILD"
-175
View File
@@ -1,175 +0,0 @@
name: "raylib ui preview"
on:
push:
branches:
- master
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]
branches:
- 'master'
paths:
- 'selfdrive/assets/**'
- 'selfdrive/ui/**'
- 'system/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create raylib UI Report"
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui"
jobs:
preview:
if: github.repository == 'commaai/openpilot'
name: preview
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
pull-requests: write
actions: read
steps:
- name: Waiting for ui generation to start
run: sleep 30
- name: Waiting for ui generation to end
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ env.SHA }}
check-name: ${{ env.UI_JOB_NAME }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
allowed-conclusions: success
wait-interval: 20
- name: Getting workflow run ID
id: get_run_id
run: |
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT
- name: Getting proposed ui
id: download-artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
run_id: ${{ steps.get_run_id.outputs.run_id }}
search_artifacts: true
name: raylib-report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master ui
uses: actions/checkout@v4
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/master_ui_raylib
ref: openpilot_master_ui_raylib
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git checkout --orphan=new_master_ui_raylib
git rm -rf *
git branch -D openpilot_master_ui_raylib
git branch -m openpilot_master_ui_raylib
git config user.name "GitHub Actions Bot"
git config user.email "<>"
mv ${{ github.workspace }}/pr_ui/*.png .
git add .
git commit -m "raylib screenshots for commit ${{ env.SHA }}"
git push origin openpilot_master_ui_raylib --force
- name: Finding diff
if: github.event_name == 'pull_request_target'
id: find_diff
run: >-
sudo apt-get update && sudo apt-get install -y imagemagick
scenes=$(find ${{ github.workspace }}/pr_ui/*.png -type f -printf "%f\n" | cut -d '.' -f 1 | grep -v 'pair_device')
A=($scenes)
DIFF=""
TABLE="<details><summary>All Screenshots</summary>"
TABLE="${TABLE}<table>"
for ((i=0; i<${#A[*]}; i=i+1));
do
# Check if the master file exists
if [ ! -f "${{ github.workspace }}/master_ui_raylib/${A[$i]}.png" ]; then
# This is a new file in PR UI that doesn't exist in master
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
DIFF="${DIFF}<table>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
DIFF="${DIFF}</tr>"
DIFF="${DIFF}</table>"
DIFF="${DIFF}</details>"
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png
composite mask.png ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png
convert -delay 100 ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif
mv ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$</summary>"
DIFF="${DIFF}<table>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> master <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>"
DIFF="${DIFF} <td> proposed <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
DIFF="${DIFF}</tr>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.png\"> </td>"
DIFF="${DIFF} <td> composite diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.gif\"> </td>"
DIFF="${DIFF}</tr>"
DIFF="${DIFF}</table>"
DIFF="${DIFF}</details>"
else
rm -f ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png
fi
INDEX=$(($i % 2))
if [[ $INDEX -eq 0 ]]; then
TABLE="${TABLE}<tr>"
fi
TABLE="${TABLE} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
if [[ $INDEX -eq 1 || $(($i + 1)) -eq ${#A[*]} ]]; then
TABLE="${TABLE}</tr>"
fi
done
TABLE="${TABLE}</table></details>"
echo "DIFF=$DIFF$TABLE" >> "$GITHUB_OUTPUT"
- name: Saving proposed ui
if: github.event_name == 'pull_request_target'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git config user.name "GitHub Actions Bot"
git config user.email "<>"
git checkout --orphan=${{ env.BRANCH_NAME }}
git rm -rf *
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "raylib screenshots for PR #${{ github.event.number }}"
git push origin ${{ env.BRANCH_NAME }} --force
- name: Comment Screenshots on PR
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
<!-- _(run_id_screenshots_raylib **${{ github.run_id }}**)_ -->
## raylib UI Preview
${{ steps.find_diff.outputs.DIFF }}
comment_tag: run_id_screenshots_raylib
pr_number: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-42
View File
@@ -1,42 +0,0 @@
name: release
on:
schedule:
- cron: '0 9 * * *'
workflow_dispatch:
jobs:
build_masterci:
name: build master-ci
env:
ImageOS: ubuntu24
container:
image: ghcr.io/commaai/openpilot-base:latest
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
permissions:
checks: read
contents: write
steps:
- name: Install wait-on-check-action dependencies
run: |
sudo apt-get update
sudo apt-get install -y libyaml-dev
- name: Wait for green check mark
if: ${{ github.event_name == 'schedule' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master
wait-interval: 30
running-workflow-name: 'build master-ci'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build prebuilt).*).)*$
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- name: Pull LFS
run: |
git config --global --add safe.directory '*'
git lfs pull
- name: Push master-ci
run: BRANCH=__nightly release/build_stripped.sh
-72
View File
@@ -1,72 +0,0 @@
name: repo maintenance
on:
schedule:
- cron: "0 14 * * 1" # every Monday at 2am UTC (6am PST)
workflow_dispatch:
env:
BASE_IMAGE: openpilot-base
BUILD: selfdrive/test/docker_build.sh base
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
jobs:
update_translations:
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
steps:
- uses: actions/checkout@v4
- uses: ./.github/workflows/setup-with-retry
- name: Update translations
run: |
${{ env.RUN }} "python3 selfdrive/ui/update_translations.py --vanish"
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
author: Vehicle Researcher <user@comma.ai>
commit-message: "Update translations"
title: "[bot] Update translations"
body: "Automatic PR from repo-maintenance -> update_translations"
branch: "update-translations"
base: "master"
delete-branch: true
labels: bot
package_updates:
name: package_updates
runs-on: ubuntu-latest
container:
image: ghcr.io/commaai/openpilot-base:latest
if: github.repository == 'commaai/openpilot'
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: uv lock
run: |
python3 -m ensurepip --upgrade
pip3 install uv
uv lock --upgrade
- name: bump submodules
run: |
git config --global --add safe.directory '*'
git submodule update --remote
git add .
- name: update car docs
run: |
export PYTHONPATH="$PWD"
scons -j$(nproc) --minimal opendbc_repo
python selfdrive/car/docs.py
git add docs/CARS.md
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
author: Vehicle Researcher <user@comma.ai>
token: ${{ secrets.ACTIONS_CREATE_PR_PAT }}
commit-message: Update Python packages
title: '[bot] Update Python packages'
branch: auto-package-updates
base: master
delete-branch: true
body: 'Automatic PR from repo-maintenance -> package_updates'
labels: bot
-71
View File
@@ -1,71 +0,0 @@
name: Schedule StarPilot Update
on:
workflow_dispatch:
inputs:
scheduled_date:
description: "Enter the date to update the \"StarPilot\" branch (YYYY-MM-DD)"
required: true
env:
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
TARGET_BRANCH: "StarPilot-Staging"
UPDATE_FILE_PATH: ".github/update_date"
jobs:
schedule_update:
runs-on: ubuntu-latest
steps:
- name: Checkout Target Branch
uses: actions/checkout@v4
with:
ref: ${{ env.TARGET_BRANCH }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 3
- name: Configure Git Identity
run: |
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
- name: Write Schedule Date
env:
SCHEDULED_DATE: ${{ github.event.inputs.scheduled_date }}
run: |
echo "$SCHEDULED_DATE" > "$UPDATE_FILE_PATH"
git add "$UPDATE_FILE_PATH"
- name: Get Target Commit Data
id: get_target
run: |
TARGET_COMMIT=$(git rev-parse HEAD~1)
AUTHOR_DATE=$(git show -s --format=%aD "$TARGET_COMMIT")
COMMITTER_DATE=$(git show -s --format=%cD "$TARGET_COMMIT")
echo "AUTHOR_DATE=$AUTHOR_DATE" >> "$GITHUB_ENV"
echo "COMMITTER_DATE=$COMMITTER_DATE" >> "$GITHUB_ENV"
echo "TARGET_COMMIT=$TARGET_COMMIT" >> "$GITHUB_ENV"
- name: Create Fixup Commit
id: fixup_commit
run: |
if git diff --cached --quiet; then
echo "No changes detected."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected. Creating fixup commit."
git commit --fixup="$TARGET_COMMIT"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Autosquash and Restore Timestamps
if: steps.fixup_commit.outputs.has_changes == 'true'
run: |
GIT_SEQUENCE_EDITOR=: git rebase --autosquash -i HEAD~3
git rebase --exec "GIT_COMMITTER_DATE='$COMMITTER_DATE' git commit --amend --no-edit --date='$AUTHOR_DATE'" HEAD~2
- name: Push Changes
if: steps.fixup_commit.outputs.has_changes == 'true'
run: |
git push origin "$TARGET_BRANCH" --force-with-lease
@@ -1,52 +0,0 @@
name: 'openpilot env setup, with retry on failure'
inputs:
docker_hub_pat:
description: 'Auth token for Docker Hub, required for BuildJet jobs'
required: false
default: ''
sleep_time:
description: 'Time to sleep between retries'
required: false
default: 30
outputs:
duration:
description: 'Duration of the setup process in seconds'
value: ${{ steps.get_duration.outputs.duration }}
runs:
using: "composite"
steps:
- id: start_time
shell: bash
run: echo "START_TIME=$(date +%s)" >> $GITHUB_ENV
- id: setup1
uses: ./.github/workflows/setup
continue-on-error: true
with:
is_retried: true
- if: steps.setup1.outcome == 'failure'
shell: bash
run: sleep ${{ inputs.sleep_time }}
- id: setup2
if: steps.setup1.outcome == 'failure'
uses: ./.github/workflows/setup
continue-on-error: true
with:
is_retried: true
- if: steps.setup2.outcome == 'failure'
shell: bash
run: sleep ${{ inputs.sleep_time }}
- id: setup3
if: steps.setup2.outcome == 'failure'
uses: ./.github/workflows/setup
with:
is_retried: true
- id: get_duration
shell: bash
run: |
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "Total duration: $DURATION seconds"
echo "duration=$DURATION" >> $GITHUB_OUTPUT
-56
View File
@@ -1,56 +0,0 @@
name: 'openpilot env setup'
inputs:
is_retried:
description: 'A mock param that asserts that we use the setup-with-retry instead of this action directly'
required: false
default: 'false'
runs:
using: "composite"
steps:
# assert that this action is retried using the setup-with-retry
- shell: bash
if: ${{ inputs.is_retried == 'false' }}
run: |
echo "You should not run this action directly. Use setup-with-retry instead"
exit 1
- shell: bash
name: No retries!
run: |
if [ "${{ github.run_attempt }}" -gt 1 ]; then
echo -e "\033[0;31m##################################################"
echo -e "\033[0;31m Retries not allowed! Fix the flaky test! "
echo -e "\033[0;31m##################################################\033[0m"
exit 1
fi
# do this after checkout to ensure our custom LFS config is used to pull from GitLab
- shell: bash
run: git lfs pull
# build cache
- id: date
shell: bash
run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV
- shell: bash
run: echo "$CACHE_COMMIT_DATE"
- id: scons-cache
uses: ./.github/workflows/auto-cache
with:
path: .ci_cache/scons_cache
key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
restore-keys: |
scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}
scons-${{ runner.arch }}
# as suggested here: https://github.com/moby/moby/issues/32816#issuecomment-910030001
- id: normalize-file-permissions
shell: bash
name: Normalize file permissions to ensure a consistent docker build cache
run: |
find . -type f -executable -not -perm 755 -exec chmod 755 {} \;
find . -type f -not -executable -not -perm 644 -exec chmod 644 {} \;
# build our docker image
- shell: bash
run: eval ${{ env.BUILD }}
-52
View File
@@ -1,52 +0,0 @@
name: stale
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
env:
DAYS_BEFORE_PR_CLOSE: 7
DAYS_BEFORE_PR_STALE: 24
DAYS_BEFORE_PR_STALE_DRAFT: 30
jobs:
stale:
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 }} 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 }}
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
-294
View File
@@ -1,294 +0,0 @@
name: tests
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
workflow_call:
inputs:
run_number:
default: '1'
required: true
type: string
concurrency:
group: tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:
PYTHONWARNINGS: error
BASE_IMAGE: openpilot-base
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: selfdrive/test/docker_build.sh base
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
jobs:
build_release:
name: build release
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
env:
STRIPPED_DIR: /tmp/releasepilot
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Getting LFS files
uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e
with:
timeout_minutes: 2
max_attempts: 3
command: git lfs pull
- name: Build devel
timeout-minutes: 1
run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot and run checks
timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
run: |
cd $STRIPPED_DIR
${{ env.RUN }} "python3 system/manager/build.py"
- name: Run tests
timeout-minutes: 1
run: |
cd $STRIPPED_DIR
${{ env.RUN }} "release/check-dirty.sh"
- name: Check submodules
if: github.repository == 'commaai/openpilot'
timeout-minutes: 3
run: release/check-submodules.sh
build:
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup docker push
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
run: |
echo "PUSH_IMAGE=true" >> "$GITHUB_ENV"
$DOCKER_LOGIN
- uses: ./.github/workflows/setup-with-retry
- uses: ./.github/workflows/compile-openpilot
timeout-minutes: 30
build_mac:
name: build macOS
if: false # tmp disable due to brew install not working
runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV
- name: Homebrew cache
uses: ./.github/workflows/auto-cache
with:
path: ~/Library/Caches/Homebrew
key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
restore-keys: |
brew-macos-${{ env.CACHE_COMMIT_DATE }}
brew-macos
- name: Install dependencies
run: ./tools/mac_setup.sh
env:
PYTHONWARNINGS: default # package install has DeprecationWarnings
HOMEBREW_DISPLAY_INSTALL_TIMES: 1
- run: git lfs pull
- name: Getting scons cache
uses: ./.github/workflows/auto-cache
with:
path: /tmp/scons_cache
key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
restore-keys: |
scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}
scons-${{ runner.arch }}-macos
- name: Building openpilot
run: . .venv/bin/activate && scons -j$(nproc)
static_analysis:
name: static analysis
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
env:
PYTHONWARNINGS: default
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Static analysis
timeout-minutes: 1
run: ${{ env.RUN }} "scripts/lint/lint.sh"
unit_tests:
name: unit tests
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
id: setup-step
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Run unit tests
timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
run: |
${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \
# Pre-compile Python bytecode so each pytest worker doesn't need to
$PYTEST --collect-only -m 'not slow' -qq && \
MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
chmod -R 777 /tmp/comma_download_cache"
process_replay:
name: process replay
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
id: setup-step
- name: Cache test routes
id: dependency-cache
uses: actions/cache@v4
with:
path: .ci_cache/comma_download_cache
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }}
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Run replay
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
run: |
${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache"
- name: Print diff
id: print-diff
if: always()
run: cat selfdrive/test/process_replay/diff.txt
- uses: actions/upload-artifact@v4
if: always()
continue-on-error: true
with:
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
run: |
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- name: Run regen
if: false
timeout-minutes: 4
run: |
${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \
chmod -R 777 /tmp/comma_download_cache"
simulator_driving:
name: simulator driving
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
if: false # FIXME: Started to timeout recently
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
id: setup-step
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Driving test
timeout-minutes: ${{ (steps.setup-step.outputs.duration < 18) && 1 || 2 }}
run: |
${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \
source selfdrive/test/setup_vsound.sh && \
CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py"
create_raylib_ui_report:
name: Create raylib UI Report
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Create raylib UI Report
run: >
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
source selfdrive/test/setup_xvfb.sh &&
python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py"
- name: Upload Raylib UI Report
uses: actions/upload-artifact@v4
with:
name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/test_ui/raylib_report/screenshots
create_mici_raylib_ui_report:
name: Create mici raylib UI Report
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Create mici raylib UI Report
run: >
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
source selfdrive/test/setup_xvfb.sh &&
WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py"
- name: Upload Raylib UI Report
uses: actions/upload-artifact@v4
with:
name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/diff/report
@@ -1,107 +0,0 @@
name: Update StarPilot Branch
on:
schedule:
- cron: "0 18 * * 6"
env:
BRANCH_STARPILOT: StarPilot
BRANCH_PREVIOUS: StarPilot-Previous
BRANCH_STAGING: StarPilot-Staging
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
TZ: America/Phoenix
UPDATE_FILE: .github/update_date
jobs:
check_update:
runs-on: ubuntu-latest
outputs:
update_due: ${{ steps.check_update.outputs.update_due }}
scheduled_date: ${{ steps.check_update.outputs.scheduled_date }}
steps:
- name: Check Update Status
id: check_update
env:
REPO_NAME: ${{ github.repository }}
run: |
URL="https://raw.githubusercontent.com/$REPO_NAME/$BRANCH_STAGING/$UPDATE_FILE"
STATUS=$(curl -o /dev/null -s -w "%{http_code}\n" "$URL")
if [ "$STATUS" != "200" ]; then
echo "update_due=false" >> "$GITHUB_OUTPUT"
exit 0
fi
SCHEDULED_DATE=$(curl -s "$URL")
CURRENT_DATE=$(TZ="$TZ" date +%F)
if [ "$SCHEDULED_DATE" == "$CURRENT_DATE" ]; then
echo "update_due=true" >> "$GITHUB_OUTPUT"
echo "scheduled_date=$SCHEDULED_DATE" >> "$GITHUB_OUTPUT"
else
echo "update_due=false" >> "$GITHUB_OUTPUT"
fi
update_branch:
needs: check_update
if: ${{ needs.check_update.outputs.update_due == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Staging
uses: actions/checkout@v4
with:
ref: ${{ env.BRANCH_STAGING }}
fetch-depth: 0
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Configure Git Identity
run: |
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
- name: Update README and Cleanup
env:
SCHEDULED_DATE: ${{ needs.check_update.outputs.scheduled_date }}
run: |
DAY=$(TZ="$TZ" date +'%d' | sed 's/^0//')
case "$DAY" in
1|21|31) SUFFIX="st" ;;
2|22) SUFFIX="nd" ;;
3|23) SUFFIX="rd" ;;
*) SUFFIX="th" ;;
esac
MONTH=$(TZ="$TZ" date +'%B')
YEAR=$(TZ="$TZ" date +'%Y')
DATE_FMT="${MONTH} ${DAY}${SUFFIX}, ${YEAR}"
DATE_ESCAPED=$(printf '%s' "$DATE_FMT" | sed -E 's/ /%20/g; s/,/%2C/g')
sed -i -E "s|(Last%20Updated-)[^-)]*|\1${DATE_ESCAPED}|g" README.md
git rm -f "$UPDATE_FILE"
git add README.md
git commit -m "Updated README date to ${DATE_FMT}"
git reset --soft HEAD~2
ORIGINAL_MSG=$(git log -1 --pretty=%B HEAD)
COMMIT_PHX=$(TZ="$TZ" date -d "$SCHEDULED_DATE 12:00" +"%Y-%m-%dT%H:%M:%S %z")
GIT_COMMITTER_DATE="$COMMIT_PHX" GIT_AUTHOR_DATE="$COMMIT_PHX" git commit -m "$ORIGINAL_MSG"
- name: Wait Until Noon ${{ env.TZ }}
run: |
NOW=$(TZ="$TZ" date +%s)
TARGET=$(TZ="$TZ" date -d "12:00" +%s)
if [ "$NOW" -lt "$TARGET" ]; then
sleep $((TARGET - NOW))
fi
- name: Push and Sync Branches
run: |
git push origin "$BRANCH_STAGING" --force
git fetch origin "$BRANCH_STARPILOT:$BRANCH_STARPILOT"
git push origin "$BRANCH_STARPILOT:$BRANCH_PREVIOUS" --force
git push origin "$BRANCH_STAGING:$BRANCH_STARPILOT" --force
-95
View File
@@ -1,95 +0,0 @@
name: Update Tinygrad
on:
workflow_dispatch:
inputs:
runner:
description: "Select runner"
type: choice
options:
- c3
- c3x
default: "c3"
required: true
env:
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
GITLAB_REPO_DIR: "StarPilot-Resources"
GITLAB_URL: "gitlab.com/FrogAi/StarPilot-Resources.git"
OPENPILOT_DIR: "/data/openpilot"
jobs:
update_tinygrad:
runs-on: [self-hosted, "${{ inputs.runner }}"]
steps:
- name: Get Version
id: get_version
run: |
VERSION=$(grep -oP '^VERSION\s*=\s*"\K[^"]+' "$OPENPILOT_DIR/starpilot/assets/model_manager.py")
echo "VERSION=$VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Setup Workspace and Clone GitLab
id: setup
env:
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
run: |
WORK_DIR="$RUNNER_TEMP/starpilot_tinygrad"
rm -rf "$WORK_DIR" && mkdir -p "$WORK_DIR"
echo "work_dir=$WORK_DIR" >> "$GITHUB_OUTPUT"
cd "$WORK_DIR"
git clone --depth 1 --branch Tinygrad "https://oauth2:${GITLAB_TOKEN}@$GITLAB_URL"
- name: Create Tinygrad Archive
working-directory: ${{ env.OPENPILOT_DIR }}
env:
WORK_DIR: ${{ steps.setup.outputs.work_dir }}
VERSION: ${{ steps.get_version.outputs.version }}
run: |
set -euo pipefail
ARCHIVE_DEST="$WORK_DIR/$GITLAB_REPO_DIR"
ARCHIVE_NAME="Tinygrad_$VERSION.tar.gz"
DUMMY_DIR=$(mktemp -d)
touch "$DUMMY_DIR/SConscript"
tar -czf "$ARCHIVE_DEST/$ARCHIVE_NAME" \
--exclude="*.a" \
--exclude="*.o" \
--exclude="*.onnx" \
--exclude="*__pycache__*" \
--exclude="*tests*" \
--exclude="selfdrive/modeld/SConscript" \
selfdrive/modeld tinygrad_repo \
-C "$DUMMY_DIR" \
--transform 's|^SConscript$|selfdrive/modeld/SConscript|' \
SConscript
rm -rf "$DUMMY_DIR"
- name: Push Updated Tinygrad
working-directory: ${{ steps.setup.outputs.work_dir }}/${{ env.GITLAB_REPO_DIR }}
env:
VERSION: ${{ steps.get_version.outputs.version }}
run: |
git config user.name "$GIT_NAME"
git config user.email "$GIT_EMAIL"
git add Tinygrad_*.tar.gz
if git diff --staged --quiet; then
echo "No changes to commit."
else
git commit -m "Updated Tinygrad: $VERSION"
git push origin Tinygrad
fi
- name: Cleanup Temporary Files
if: always()
env:
WORK_DIR: ${{ steps.setup.outputs.work_dir }}
run: |
rm -rf "$WORK_DIR"
+5 -41
View File
@@ -13,10 +13,6 @@ venv/
a.out a.out
.hypothesis .hypothesis
.cache/ .cache/
.host_runtime/
.comma_sysroot/
.venv-linux-arm64/
compiledmodels/
/docs_site/ /docs_site/
@@ -48,13 +44,14 @@ clcache
compile_commands.json compile_commands.json
compare_runtime*.html compare_runtime*.html
selfdrive/pandad/pandad
cereal/services.h cereal/services.h
cereal/gen cereal/gen
cereal/messaging/bridge
selfdrive/ui/translations/tmp selfdrive/ui/translations/tmp
selfdrive/car/tests/cars_dump selfdrive/car/tests/cars_dump
system/camerad/camerad
system/camerad/test/ae_gray_test system/camerad/test/ae_gray_test
selfdrive/ui/ui.macos
selfdrive/ui/ui.larch64
.coverage* .coverage*
coverage.xml coverage.xml
@@ -68,16 +65,6 @@ cppcheck_report.txt
comma*.sh comma*.sh
selfdrive/modeld/models/*.pkl selfdrive/modeld/models/*.pkl
!selfdrive/modeld/models/driving_vision_tinygrad.pkl
!selfdrive/modeld/models/driving_policy_tinygrad.pkl
!selfdrive/modeld/models/driving_vision_metadata.pkl
!selfdrive/modeld/models/driving_policy_metadata.pkl
!selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl
!selfdrive/modeld/models/dmonitoring_model_metadata.pkl
!selfdrive/modeld/models/warp_1928x1208_tinygrad.pkl
!selfdrive/modeld/models/warp_1344x760_tinygrad.pkl
!selfdrive/modeld/models/dm_warp_1928x1208_tinygrad.pkl
!selfdrive/modeld/models/dm_warp_1344x760_tinygrad.pkl
# openpilot log files # openpilot log files
*.bz2 *.bz2
@@ -109,28 +96,5 @@ Pipfile
.history .history
.ionide .ionide
# Keep prebuilt runtime artifacts trackable # rick - keep panda_tici standalone
!cereal/messaging/bridge panda_tici/
!system/camerad/camerad
!system/loggerd/loggerd
!system/loggerd/encoderd
!system/loggerd/bootlog
!selfdrive/pandad/pandad
!cereal/services.h
!cereal/libcereal.a
!cereal/libsocketmaster.a
!common/params_pyx.so
!common/params_pyx.cpp
!common/transformations/transformations.so
!selfdrive/modeld/models/commonmodel_pyx.so
!selfdrive/pandad/pandad_api_impl.so
!selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so
!selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/libacados_ocp_solver_lat.so
!selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so
!selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/libacados_ocp_solver_long.so
!common/libcommon.a
!msgq_repo/msgq/ipc_pyx.so
!msgq_repo/msgq/visionipc/visionipc_pyx.so
!rednose_repo/rednose/helpers/ekf_sym_pyx.so
!panda/board/obj/
!panda/board/obj/**
+3 -1
View File
@@ -4,6 +4,8 @@
"ms-vscode.cpptools", "ms-vscode.cpptools",
"elagil.pre-commit-helper", "elagil.pre-commit-helper",
"charliermarsh.ruff", "charliermarsh.ruff",
"openai.chatgpt", "JamiTech.simply-blame",
"k--kato.intellij-idea-keybindings",
"trinm1709.dracula-theme-from-intellij"
] ]
} }
+15 -1
View File
@@ -3,10 +3,24 @@
"editor.insertSpaces": true, "editor.insertSpaces": true,
"editor.renderWhitespace": "trailing", "editor.renderWhitespace": "trailing",
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"terminal.integrated.defaultProfile.linux": "dragonpilot",
"terminal.integrated.profiles.linux": {
"dragonpilot": {
"path": "bash",
"args": ["-c", "distrobox enter dp"]
}
},
"search.exclude": { "search.exclude": {
"**/.git": true, "**/.git": true,
"**/.venv": true, "**/.venv": true,
"**/__pycache__": true "**/__pycache__": true,
"msgq_repo/": true,
"rednose/": true,
"rednose_repo/": true,
"openpilot/": true,
"teleoprtc_repo/": true,
"tinygrad/": true,
"tinygrad_repo/": true
}, },
"files.exclude": { "files.exclude": {
"**/.git": true, "**/.git": true,
+140
View File
@@ -0,0 +1,140 @@
# ALKA (Always-on Lane Keeping Assist) Design v3
## Overview
ALKA enables lateral control (steering) when ACC Main is ON, without requiring cruise to be engaged. This allows lane keeping assist to function independently of longitudinal control.
**Simplified Behavior (v3):**
- All brands use direct tracking: `lkas_on = acc_main_on`
- No button/toggle tracking (removed TJA, LKAS button, LKAS HUD)
- ACC Main ON = ALKA enabled, ACC Main OFF = ALKA disabled
---
## Per-Brand Summary
| Brand | Status | ACC Main Source | Notes |
|-------|--------|-----------------|-------|
| Body | Disabled | - | No steering capability |
| Chrysler | Disabled | - | Needs special handling |
| Ford | Enabled | EngBrakeData (0x165) CcStat | |
| GM | Disabled | - | No ACC Main signal |
| Honda Nidec | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
| Honda Bosch | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
| Hyundai | Enabled | SCC11 (0x420) bit 0 | |
| Hyundai CAN-FD | Enabled | SCC_CONTROL (0x1A0) bit 66 | |
| Hyundai Legacy | Enabled | SCC11 (0x420) bit 0 | |
| Mazda | Enabled | CRZ_CTRL (0x21C) bit 17 | |
| Nissan | Enabled | CRUISE_THROTTLE (0x239) bit 17 | |
| PSA | Disabled | - | Not implemented |
| Rivian | Disabled | - | Different architecture |
| Subaru | Enabled | CruiseControl (0x240) bit 40 | |
| Subaru Preglobal | Enabled | CruiseControl (0x144) bit 48 | |
| Tesla | Disabled | - | Different architecture |
| Toyota | Enabled | PCM_CRUISE_2 (0x1D3) bit 15 | |
| Toyota (UNSUPPORTED_DSU) | Enabled | DSU_CRUISE (0x365) bit 0 | |
| VW MQB | Enabled | TSK_06 TSK_Status (>=2) | |
| VW PQ | Enabled | Motor_5 (0x480) bit 50 (long) | |
---
## Permission Model
Lateral control requires checks at both layers. Normal path uses `controls_allowed`, ALKA path uses additional checks.
| Check | Panda | openpilot | Notes |
|-------|:-----:|:---------:|-------|
| **Normal Path** |
| `controls_allowed` (cruise engaged) | ✓ | ✓ | Either this OR ALKA path |
| **ALKA Path** |
| `alka_allowed` (brand supports) | ✓ | ✓ | Set per brand in safety init |
| `ALT_EXP_ALKA` (user enabled) | ✓ | ✓ | alternativeExperience flag |
| `lkas_on` (ACC Main ON) | ✓ | ✓ | Tracked via CAN messages |
| `vehicle_moving` / `!standstill` | ✓ | ✓ | |
| **openpilot Additional** |
| `gear_ok` (not P/N/R) | ✗ | ✓ | Python layer only |
| `calibrated` | ✗ | ✓ | Python layer only |
| `seatbelt latched` | ✗ | ✓ | Python layer only |
| `doors closed` | ✗ | ✓ | Python layer only |
| `!steerFaultTemporary` | ✗ | ✓ | Python layer only |
| `!steerFaultPermanent` | ✗ | ✓ | Python layer only |
---
## Data Flow
```
┌─────────────────────────────────────────────────────────────────────┐
│ CAN Bus │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ Safety Layer (panda C code) │ │ Python Layer │
│ │ │ │
│ rx_hook: │ │ carstate.py: │
│ - Parse ACC Main signal │ │ - Parse cruiseState.available │
│ - Set lkas_on = acc_main_on │ │ - Set self.lkas_on │
│ │ │ │
│ lat_control_allowed(): │ └─────────────┬───────────────────┘
│ - Check lkas_on + other flags │ │
│ - Gate steering commands │ ▼
└─────────────────────────────────┘ ┌─────────────────────────────────┐
│ card.py: │
│ - Publish carStateExt.lkasOn │
└─────────────┬───────────────────┘
┌─────────────────────────────────┐
│ controlsd.py: │
│ - Read carStateExt.lkasOn │
│ - Check ALKA conditions │
│ - Set CC.latActive │
└─────────────────────────────────┘
```
### Key Files
| File | Purpose |
|------|---------|
| `custom.capnp` | Defines `CarStateExt` struct with `lkasOn` field |
| `log.capnp` | Includes `carStateExt` in event union |
| `interfaces.py` | Defines `self.lkas_on = False` default in `CarStateBase` |
| `carstate.py` (per brand) | Tracks `lkas_on` based on ACC Main |
| `card.py` | Publishes `carStateExt.lkasOn` from `CI.CS.lkas_on` |
| `controlsd.py` | Reads `carStateExt.lkasOn` to determine `alka_active` |
---
## ACC Main Tracking
All brands use simple direct tracking:
```c
// Panda (C code)
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
lkas_on = acc_main_on; // or GET_BIT(msg, bit_position)
}
```
```python
# Python carstate.py
self.lkas_on = ret.cruiseState.available
```
This guard ensures:
1. Brand supports ALKA (`alka_allowed`)
2. User enabled ALKA (`ALT_EXP_ALKA`)
Without both conditions, no ACC Main tracking occurs, and ALKA remains disabled.
---
## Testing
Safety tests verify:
- `alka_allowed` flag set correctly per brand
- ACC Main tracking updates `lkas_on` directly
- `lat_control_allowed()` returns true only when all conditions met
- Steering TX blocked when ALKA conditions not met
- Bus routing variants (camera_scc, unsupported_dsu)
+91
View File
@@ -0,0 +1,91 @@
```mermaid
flowchart TD
B000["devel-staging"] ---> CORE["core"]
CORE ---> CORE_001["core-feat/params"]
CORE_001 ---> CORE_002["core-feat/panel"]
CORE_002 ---> CORE_003["core-feat/safety-ext"]
CORE_003 ---> MIN["min"]
MIN ---> MIN_001["min-feat/ui/display-mode"]
MIN ---> MIN_002["min-feat/dev/model-selector"]
MIN ---> MIN_003["min-feat/lat/lca"]
MIN ---> MIN_004["min-feat/dev/on-off-road"]
MIN ---> MIN_005["min-feat/ui/hide-hud"]
MIN ---> MIN_006["min-feat/lon/ext-radar"]
MIN ---> MIN_007["min-feat/lat/road-edge-detection"]
MIN ---> MIN_008["min-feat/ui/rainbow-path"]
MIN ---> MIN_009["min-feat/lon/acm"]
MIN ---> MIN_010["min-feat/lon/aem"]
MIN ---> MIN_011["min-feat/lon/dtsc"]
MIN ---> MIN_012["min-feat/lon/apm"]
MIN ---> MIN_013["min-feat/lon/dasr"]
MIN ---> MIN_014["min-feat/dev/alert-mode"]
MIN ---> MIN_015["min-feat/dev/auto-shutdown"]
MIN ---> MIN_016["min-feat/ui/lead-stats"]
MIN ---> MIN_017["min-feat/ui/border-indicator"]
MIN ---> MIN_018["min-feat/dev/delay-loggerd"]
MIN ---> MIN_019["min-feat/dev/disable-connect"]
MIN ---> MIN_020["min-feat/dev/tether-on-boot"]
MIN ---> MIN_021["min-feat/ui/torque-bar"]
MIN ---> MIN_022["min-feat/ui/mici-ui-mode"]
MIN ---> MIN_023["min-feat/lat/lat-offset"]
MIN_001 ---> FULL["full"]
MIN_002 ---> FULL
MIN_003 ---> FULL
MIN_004 ---> FULL
MIN_005 ---> FULL
MIN_006 ---> FULL
MIN_007 ---> FULL
MIN_008 ---> FULL
MIN_009 ---> FULL
MIN_010 ---> FULL
MIN_011 ---> FULL
MIN_012 ---> FULL
MIN_013 ---> FULL
MIN_014 ---> FULL
MIN_015 ---> FULL
MIN_016 ---> FULL
MIN_017 ---> FULL
MIN_018 ---> FULL
MIN_019 ---> FULL
MIN_020 ---> FULL
MIN_021 ---> FULL
FULL ---> TOYOTA_001[brand/toyota/safety-common]
FULL ---> TOYOTA_002[brand/toyota/door-auto-lock-unlock]
FULL ---> TOYOTA_003[brand/toyota/tss1-sng]
FULL ---> TOYOTA_004[brand/toyota/radar-filter]
FULL ---> TOYOTA_005[brand/toyota/sdsu]
FULL ---> TOYOTA_006[brand/toyota/dsu-bypass]
FULL ---> TOYOTA_007[brand/toyota/zss]
FULL ---> TOYOTA_008[brand/toyota/stock-lon]
FULL ---> VAG_001[brand/vag/a0-sng]
FULL ---> VAG_002[brand/vag/pq-steering-patch]
FULL ---> VAG_003[brand/vag/pq-no-dashcam]
FULL ---> VAG_004[brand/vag/avoid-eps-lockout]
FULL ---> HKG_001[brand/hkg/smdps]
FULL ---> HONDA_001[brand/honda/eps-mod]
FULL ---> HONDA_002[brand/honda/nidec-stock-long]
FULL ---> SUBARU_001[brand/subaru/torque-3071]
TOYOTA_001 ---> TOYOTA[pre-toyota]
TOYOTA_002 ---> TOYOTA
TOYOTA_003 ---> TOYOTA
TOYOTA_004 ---> TOYOTA
TOYOTA_005 ---> TOYOTA
TOYOTA_006 ---> TOYOTA
TOYOTA_007 ---> TOYOTA
TOYOTA_008 ---> TOYOTA
VAG_001 ---> VAG[pre-vag]
VAG_002 ---> VAG
VAG_003 ---> VAG
VAG_004 ---> VAG
HKG_001 ---> HKG[pre-hkg]
HONDA_001 ---> HONDA[pre-honda]
SUBARU_001 ---> SUBARU[pre-subaru]
TOYOTA ---> PRE[pre]
VAG ---> PRE
HKG ---> PRE
HONDA ---> PRE
SUBARU ---> PRE
PRE ---> PRE_PATCH[pre-patch]
PRE_PATCH ---> PREBUILD[pre-build]
PREBUILD ---> VERSION[x.x.x]
```
+1877
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
Copyright (c) 2019, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
Copyright (c) 2018, Comma.ai, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+59 -60
View File
@@ -1,75 +1,74 @@
# StarPilot ![](dragonpilot/selfdrive/assets/dragonpilot.png)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/firestar5683/StarPilot) [Read this in English](README_EN.md)
[![Discord](https://img.shields.io/discord/1137853399715549214?label=Discord)](https://firestar.link/discord)
[![Last Updated](https://img.shields.io/github/last-commit/firestar5683/StarPilot/StarPilot)](https://github.com/firestar5683/StarPilot)
[![Wiki](https://img.shields.io/badge/Wiki-StarPilot-blue?logo=wiki)](https://wiki.firestar.link)
**StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot), # **🐲 dragonpilot - 賦予您的愛車「龍」之魂**
an open source driver assistance system.
Openpilot provides **我們與您一同翱翔於更智慧、更貼心的駕駛旅程。**
* Automated Lane Centering
* Adaptive Cruise Control
* Lane Change Assist
* Driver Monitoring *without wheel nags*
StarPilot adds support for many GM vehicles along with improved tuning, ## **👋 嘿, 朋友,歡迎您的到來!**
especially for radar-less (camera only) vehicles.
StarPilot is built off of [StarPilot](https://github.com/FrogAi/StarPilot) `dragonpilot` 誕生於 2019 年,由三位早期的 openpilot 華人玩家共同創立。初衷很簡單:為廣大的華人用戶、玩家們提供一個友善的交流環境、更簡便的設定協助,並加入更多適合在地使用的貼心功能。
and supports the major features StarPilot offers.
StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord). 我們深知在地化的重要性,特別是語言的親切感。因此,我們率先導入了完整的中文介面,讓 `dragonpilot` 迅速在華語地區累積了口碑,也讓華人的使用者數量在全球名列前茅。這份來自在地的支持,是我們持續前進的最大動力。
Stop by to chat or ask questions!
## Documentation 我們以功能強大的 [openpilot](https://github.com/commaai/openpilot) 為基礎——這套據美國消費者報告評測優於市售車方案的開源輔助駕駛系統——融入了更多在地化的巧思與客製化的溫度,希望能打造出最符合您需求的駕駛夥伴。(您也可以參考我們 repo 中保留的 [openpilot 原始說明檔案](README_OPENPILOT.md))
Please see [https://wiki.firestar.link](https://wiki.firestar.link) for hardware lists, 取名 `dragonpilot`,是因為我們希望它能像神話中的「龍」一樣,既強大又充滿智慧,為您的行車安全保駕護航。龍,在我們華人文化中,更是吉祥與力量的象徵,也代表著我們的根源與驕傲。
installation guides, and software configuration.
## Features ## **✨ dragonpilot 的里程碑**
* Full support for Comma C3, C3X, and C4 我們不僅保留了 openpilot 的核心優勢,更達成了許多從社群回饋中誕生的里程碑,這些是我們引以為傲的足跡:
* C4 is currently in release testing. Join our fleet of C4 testers!
* Model switcher with all of comma's tinygrad driving models
* Special longitudinal planner tuning for VoACC (visual only, radar-less) vehicles
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
Download models, change settings, update software, visualize live model outputs for tuning.
* Always On Lateral (full time steering assist)*
* Speed Limit Controller*
* Learning Curve Speed Controller*
* Conditional Experimental Mode (CEM)*
* Driving Profiles*
* Custom themes*
* Alert Volume Controller*
* Comma Pedal Interceptor support*
* Toyota SDSU support*
* ZSS support*
* High quality dashcam recordings*
* Enhanced tuning for CEM (dynamic experimental mode switching)
\* [Inherited from StarPilot](https://github.com/FrogAi/StarPilot#openpilot-vs-starpilot) * **🚘 全時置中車道維持 (ALKA)**
## GM-only Features 這不只是一個功能,更是 `dragonpilot` 的哲學。我們最早於 [0.6.2 版本](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199) 時便實現了這個功能,其開發歷程始於 2017 Lexus IS300h,接著擴展至 Toyota 全車系,並逐步延伸到其他支援的品牌。它能溫柔地輔助您,讓車輛始終穩定地保持在車道中央,提供一份額外的安心與從容。
* Increased LKAS fault resiliency * **🌐 率先導入多國語言介面**
* ASCM_INT and SASCM support
* Custom lateral torque controller, with special tuning for Bolts
* 50% extra torque on 2017 Chevy Bolt
* Improved lateral and longitudinal tuning
* Dashboard cruise control display speed spoofing for vehicles with pedal interceptor
* Extra steering wheel button functionality for vehicles with pedal interceptor
* Optional toggle to boot comma when remote starting your vehicle
## Developer Features 在官方 openpilot 還未支援前,我們便已將多國語言介面實現。`dragonpilot` 完整支援繁體中文、簡體中文與英文,讓操作毫無隔閡。
* Native and cross compilation for Windows, Mac, and Ubuntu * **💻 唯一同時支援多硬體平台**
* Custom AGNOS to support C3, C3X, and C4
* To run UI on PC: 我們是唯一曾致力於讓專案同時兼容 EON、comma two、comma 3 與 Jetson 平台的社群分支,這份努力是為了服務最廣大的玩家社群。
* `./c3` for large UI 此外,在 comma.ai 團隊於 0.10.0 版本宣布停止支持 comma 3 後,我們仍是唯一一個完整同時支援 comma 3、comma 3X 以及 O3、O3L、O3XL(O3 系列為副廠硬體)的社群分支。
* `./c4` for small UI
* `./build` to produce cross compiled binaries for comma devices. * **📜 曾榮獲官方認證第一大分支**
Uses your comma's sysroot/toolchain
* Toggle: "Use Precompiled Binaries" to allow switching between fast boot / editable builds 基於活躍的社群與功能創新,`dragonpilot` 曾一度成長為 comma ai 官方認證的第一大 openpilot 分支,這份榮耀屬於每一位參與者。
* Custom long maneuver tests, specifically designed for regen-only vehicles
## **🧑‍💻 設計理念 - 少即是多 (Less is More)**
隨著 openpilot 的 AI 模型日益強大,許多過去需要手動微調的功能,現在都已能透過更先進的模型來實現。因此,我們現在的開發重心回歸到 **「最小化修改」(minimal changes)** 的核心原則上。
我們的目標是為您提供最純粹、最接近官方的 openpilot 駕駛感受,同時保留 `dragonpilot` 那些經過時間考驗、最受社群喜愛的經典功能。我們相信,在強大的 AI 基礎上,簡潔即是力量。
## **🛠️ 硬件的足跡 - 一路走來的夥伴們**
從最早的 **EON**,到官方的 **comma two / three (C2/C3/C3X)**,再到社群中各式各樣充滿智慧的**副廠機 (如 C1.5, O2, O3, O3L, O3XL 等)**,甚至我們也曾探索過在 [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot) 上的可能性。
目前最新版本主要支援: comma3 / 3X 以及 O3 / O3L / O3XL 等社群硬體。
針對 EON / C1.5 / C2 等舊款硬體,最後支援的版本位於 [d2 分支](https://github.com/dragonpilot-community/dragonpilot/tree/d2)。
無論您手上是哪一款設備,都代表著您對開源駕駛輔助的一份熱情。
## **🫂 加入我們,成為「尋龍者」的一份子**
`dragonpilot` 的成長,離不開每一位使用者的貢獻與回饋。我們是一個以**公開、透明**為原則的溫暖社群,希望在這裡能與所有對 openpilot / dragonpilot 有興趣的用戶分享、交流開發與使用上的經驗。
[**歡迎加入我們的 Facebook 社團進行交流!**](https://www.facebook.com/groups/930190251238639)
## **❤️ 特別感謝**
`dragonpilot` 從創立至今,從未打算透過 Patreon 等平台進行任何形式的募資。我們的初衷是建立一個讓大家能一起學習、一起成長的社群。It's all about fun, not money.
然而,我們仍要對那些自發性支持本專案的朋友們,致上最誠摯的感謝。正是因為有您們的鼓勵,我們才有更大的動力持續前進。
[**我們的贊助者名單**](SPONSORS.md)
### **安全聲明**
`dragonpilot` 是一種駕駛**輔助**系統,並非全自動駕駛。它旨在減輕您的駕駛疲勞,提升行車安全,但駕駛人仍需時刻保持專注,並隨時準備接管車輛。請務必遵守您所在地區的交通法規。
**最後,再次感謝您的到來。**
**期待與您一同在智慧駕駛的道路上,乘「龍」而行!**
+74
View File
@@ -0,0 +1,74 @@
![](dragonpilot/selfdrive/assets/dragonpilot.png)
[Read this in Chinese](README.md)
# **🐲 dragonpilot - Bringing the Spirit of the Dragon to Your Car**
**Join us on a smarter, more thoughtful driving journey.**
## **👋 Welcome, friend!**
`dragonpilot` was launched in 2019 by three early openpilot enthusiasts from the Chinese community. Our mission was simple: create a friendly space for users to share experiences, provide easier setup help, and add features tailored for local needs.
Localization has always been at the heart of what we do—starting with a fully Chinese interface. This made `dragonpilot` quickly popular in Chinese-speaking regions and helped our user base grow into one of the largest worldwide. That community support is what keeps us moving forward.
Built on top of the powerful [openpilot](https://github.com/commaai/openpilot)—an open-source driver assistance system rated by Consumer Reports as outperforming commercial offerings—we add localized refinements and user-focused features to create a driving companion that truly fits your needs. (You can also see the [original openpilot README](README_OPENPILOT.md) preserved in our repo.)
The name `dragonpilot` reflects our vision: like the dragon of mythology, it is strong and wise, guarding your safety on the road. In Chinese culture, the dragon is also a symbol of luck and strength, representing our roots and pride.
## **✨ Milestones**
Beyond carrying forward openpilot's core strengths, we've reached several milestones inspired by community feedback:
* **🚘 Always Lane Keep Assist (ALKA)**
More than a feature—it's part of the `dragonpilot` philosophy. Introduced as early as [version 0.6.2](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199), first tested on a 2017 Lexus IS300h, then expanded to Toyota's lineup and beyond. ALKA helps keep your vehicle steadily centered, giving you extra confidence on the road.
* **🌐 First to add multilingual support**
Before openpilot officially supported it, we had already introduced multiple languages. `dragonpilot` fully supports Traditional Chinese, Simplified Chinese, and English.
* **💻 Only community fork to support multiple hardware platforms at once**
We uniquely worked to make the project run on EON, comma two, comma 3, and Jetson—serving the widest range of users possible.
Additionally, after the comma.ai team deprecated the comma 3 in version 0.10.0, we remain the only community fork to offer full, simultaneous support for the comma 3, comma 3X, and the O3, O3L, and O3XL (the O3 series being third-party hardware).
* **📜 Once recognized as the #1 openpilot fork**
Thanks to an active community and continuous innovation, `dragonpilot` was once the largest openpilot fork officially recognized by comma ai. This honor belongs to everyone who contributed.
## **🧑‍💻 Design Philosophy - Less is More**
As openpilot's AI grows stronger, many features that once required manual tuning are now handled by advanced models. That's why our focus has returned to **“minimal changes.”**
We aim to give you the purest, most official-like openpilot driving experience—while preserving `dragonpilot`'s classic, community-loved features. With a solid AI foundation, simplicity is strength.
## **🛠️ Hardware Journey**
From the early **EON**, to official devices like **comma two / three (C2/C3/C3X)**, to creative community builds (**C1.5, O2, O3, O3L, O3XL, etc.**), and even experiments with [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot).
Currently, the latest versions support: **comma3 / 3X** and community hardware like **O3 / O3L / O3XL**.
Older devices such as **EON / C1.5 / C2** are supported in the [d2 branch](https://github.com/dragonpilot-community/dragonpilot/tree/d2).
Whatever device you're on, it represents your passion for open-source driver assistance.
## **🫂 Join Us Become a “Dragon Seeker”**
`dragonpilot` thrives thanks to every user's contributions and feedback. We're an open, transparent, and welcoming community where enthusiasts can share experiences with openpilot and `dragonpilot`.
[**Join our Facebook group here!**](https://www.facebook.com/groups/930190251238639)
## **❤️ Special Thanks**
Since day one, `dragonpilot` has never asked for funding through Patreon or similar platforms. Our vision is a community where everyone learns and grows together. It's about fun, not money.
That said, we're deeply grateful to those who voluntarily supported the project. Your encouragement keeps us motivated to keep building.
[**See our sponsors**](SPONSORS.md)
### **Safety Notice**
`dragonpilot` is a driver **assistance** system, not full self-driving. It reduces fatigue and improves safety, but you must remain alert and ready to take control at all times. Always follow your local traffic laws.
**Thanks again for being here.**
**We look forward to riding the “dragon” with you on the road to smarter driving!**
+111
View File
@@ -0,0 +1,111 @@
<div align="center" style="text-align: center;">
<h1>openpilot</h1>
<p>
<b>openpilot is an operating system for robotics.</b>
<br>
Currently, it upgrades the driver assistance system in 300+ supported cars.
</p>
<h3>
<a href="https://docs.comma.ai">Docs</a>
<span> · </span>
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
<span> · </span>
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
<span> · </span>
<a href="https://discord.comma.ai">Community</a>
<span> · </span>
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
</h3>
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
[![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/tests.yaml/badge.svg)](https://github.com/commaai/openpilot/actions/workflows/tests.yaml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![X Follow](https://img.shields.io/twitter/follow/comma_ai)](https://x.com/comma_ai)
[![Discord](https://img.shields.io/discord/469524606043160576)](https://discord.comma.ai)
</div>
<table>
<tr>
<td><a href="https://youtu.be/NmBfgOanCyk" title="Video By Greer Viau"><img src="https://github.com/commaai/openpilot/assets/8762862/2f7112ae-f748-4f39-b617-fabd689c3772"></a></td>
<td><a href="https://youtu.be/VHKyqZ7t8Gw" title="Video By Logan LeGrand"><img src="https://github.com/commaai/openpilot/assets/8762862/92351544-2833-40d7-9e0b-7ef7ae37ec4c"></a></td>
<td><a href="https://youtu.be/SUIZYzxtMQs" title="A drive to Taco Bell"><img src="https://github.com/commaai/openpilot/assets/8762862/05ceefc5-2628-439c-a9b2-89ce77dc6f63"></a></td>
</tr>
</table>
Using openpilot in a car
------
To use openpilot in a car, you need four things:
1. **Supported Device:** a comma 3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x).
2. **Software:** The setup procedure for the comma 3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version.
3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md).
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3X to your car.
We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play.
### Branches
Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches:
| comma four branch | comma 3X branch | URL | description |
|------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------|
| `release-mici` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. |
| `release-mici-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. |
| `nightly` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. |
| `nightly-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. |
To start developing openpilot
------
openpilot is developed by [comma](https://comma.ai/) and by users like you. We welcome both pull requests and issues on [GitHub](http://github.com/commaai/openpilot).
* Join the [community Discord](https://discord.comma.ai)
* Check out [the contributing docs](docs/CONTRIBUTING.md)
* Check out the [openpilot tools](tools/)
* Code documentation lives at https://docs.comma.ai
* Information about running openpilot lives on the [community wiki](https://github.com/commaai/openpilot/wiki)
Want to get paid to work on openpilot? [comma is hiring](https://comma.ai/jobs#open-positions) and offers lots of [bounties](https://comma.ai/bounties) for external contributors.
Safety and Testing
----
* openpilot observes [ISO26262](https://en.wikipedia.org/wiki/ISO_26262) guidelines, see [SAFETY.md](docs/SAFETY.md) for more details.
* openpilot has software-in-the-loop [tests](.github/workflows/tests.yaml) that run on every commit.
* The code enforcing the safety model lives in panda and is written in C, see [code rigor](https://github.com/commaai/panda#code-rigor) for more details.
* panda has software-in-the-loop [safety tests](https://github.com/commaai/panda/tree/master/tests/safety).
* Internally, we have a hardware-in-the-loop Jenkins test suite that builds and unit tests the various processes.
* panda has additional hardware-in-the-loop [tests](https://github.com/commaai/panda/blob/master/Jenkinsfile).
* We run the latest openpilot in a testing closet containing 10 comma devices continuously replaying routes.
<details>
<summary>MIT Licensed</summary>
openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified.
Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys fees and costs) which arise out of, relate to or result from any use of this software by user.
**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT.
YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS.
NO WARRANTY EXPRESSED OR IMPLIED.**
</details>
<details>
<summary>User Data and comma Account</summary>
By default, openpilot uploads the driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone.
openpilot is open source software: the user is free to disable data collection if they wish to do so.
openpilot logs the road-facing cameras, 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.
By using openpilot, you agree to [our Privacy Policy](https://comma.ai/privacy). 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.
</details>
+98 -384
View File
@@ -1,266 +1,56 @@
import os import os
import shutil
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
import platform import platform
import shlex
import numpy as np import numpy as np
import SCons.Errors import SCons.Errors
SCons.Warnings.warningAsException(True) SCons.Warnings.warningAsException(True)
# pending upstream fix - https://github.com/SCons/scons/issues/4461
#SetOption('warn', 'all')
force_tici = os.environ.get("SP_FORCE_TICI", "").lower() in {"1", "true", "yes", "on"}
TICI = os.path.isfile('/TICI') or force_tici
AGNOS = TICI
Decider('MD5-timestamp') Decider('MD5-timestamp')
SetOption('num_jobs', max(1, int(os.cpu_count()/2))) SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
AddOption('--kaitai', AddOption('--kaitai', action='store_true', help='Regenerate kaitai struct parsers')
action='store_true', AddOption('--asan', action='store_true', help='turn on ASAN')
help='Regenerate kaitai struct parsers') AddOption('--ubsan', action='store_true', help='turn on UBSan')
AddOption('--mutation', action='store_true', help='generate mutation-ready code')
AddOption('--asan', AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line')
action='store_true',
help='turn on ASAN')
AddOption('--ubsan',
action='store_true',
help='turn on UBSan')
AddOption('--coverage',
action='store_true',
help='build with test coverage options')
AddOption('--clazy',
action='store_true',
help='build with clazy')
AddOption('--ccflags',
action='store',
type='string',
default='',
help='pass arbitrary flags over the command line')
AddOption('--external-sconscript',
action='store',
metavar='FILE',
dest='external_sconscript',
help='add an external SConscript to the build')
AddOption('--mutation',
action='store_true',
help='generate mutation-ready code')
AddOption('--minimal', AddOption('--minimal',
action='store_false', action='store_false',
dest='extras', dest='extras',
default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS) default=os.path.exists(File('#.gitattributes').abspath), # minimal by default on release branch (where there's no LFS)
help='the minimum build to run openpilot. no tests, tools, etc.') help='the minimum build to run openpilot. no tests, tools, etc.')
AddOption('--extras', # Detect platform
action='store_true', arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
dest='extras', if platform.system() == "Darwin":
default=os.path.exists(File('#.lfsconfig').abspath),
help='build optional tools/tests even when minimal is the default')
def maybe_delegate_to_laptop_device_builder() -> None:
if platform.system() != "Darwin":
return
if os.environ.get("SP_FORCE_ARCH"):
return
if os.environ.get("SP_SKIP_CONTAINER_REEXEC"):
return
if os.environ.get("SP_DISABLE_AUTO_DEVICE_SCONS", "").lower() in {"1", "true", "yes", "on"}:
return
basedir = Dir("#").abspath
sysroot_dir = os.environ.get("COMMA_SYSROOT_DIR", os.path.join(basedir, ".comma_sysroot"))
required_sysroot_dirs = (
"usr/local/lib",
"lib/aarch64-linux-gnu",
"usr/lib/aarch64-linux-gnu",
"system/vendor/lib64",
)
if not all(os.path.isdir(os.path.join(sysroot_dir, p)) for p in required_sysroot_dirs):
return
docker_bin = shutil.which("docker")
if docker_bin is None:
mac_docker = "/Applications/Docker.app/Contents/Resources/bin/docker"
if os.path.isfile(mac_docker):
docker_bin = mac_docker
if docker_bin is None and shutil.which("podman") is None:
return
builder = os.path.join(basedir, "scripts", "laptop_device_build.sh")
if not os.path.isfile(builder):
return
print(f"Auto-routing scons to laptop device build (sysroot: {sysroot_dir})", flush=True)
env = os.environ.copy()
env["SP_SKIP_CONTAINER_REEXEC"] = "1"
env.setdefault("COMMA_SYSROOT_DIR", sysroot_dir)
if docker_bin is not None and shutil.which("docker") is None:
docker_dir = os.path.dirname(docker_bin)
env["PATH"] = f"{docker_dir}:{env.get('PATH', '')}"
cmd = [builder, "build", *sys.argv[1:]]
raise SystemExit(subprocess.call(cmd, cwd=basedir, env=env))
maybe_delegate_to_laptop_device_builder()
## Architecture name breakdown (arch)
## - larch64: linux tici aarch64
## - aarch64: linux pc aarch64
## - x86_64: linux pc x64
## - Darwin: mac x64 or arm64
real_arch = arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
forced_arch = os.environ.get("SP_FORCE_ARCH")
if forced_arch:
arch = forced_arch
elif platform.system() == "Darwin":
arch = "Darwin" arch = "Darwin"
brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip()
elif arch == "aarch64" and AGNOS: elif arch == "aarch64" and os.path.isfile('/TICI'):
arch = "larch64" arch = "larch64"
assert arch in ["larch64", "aarch64", "x86_64", "Darwin"] assert arch in [
"larch64", # linux tici arm64
# Homebrew llvm can shadow Apple clang and break macOS SDK header resolution. "aarch64", # linux pc arm64
# Use the system toolchain explicitly on macOS for reliable local builds. "x86_64", # linux pc x64
cc = '/usr/bin/clang' if arch == "Darwin" else 'clang' "Darwin", # macOS arm64 (x86 not supported)
cxx = '/usr/bin/clang++' if arch == "Darwin" else 'clang++' ]
ar = '/usr/bin/ar' if arch == "Darwin" else 'ar'
ranlib = '/usr/bin/ranlib' if arch == "Darwin" else 'ranlib'
lenv = {
"PATH": os.environ['PATH'],
"PYTHONPATH": ":".join([
Dir("#").abspath,
Dir("#third_party/acados").abspath,
Dir("#opendbc_repo").abspath,
]),
"ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath,
"ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath,
"TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer"
}
# Allow callers to override cache/temp dirs used by subprocesses (e.g. tinygrad model compilation).
for key in ("HOME", "TMPDIR", "XDG_CACHE_HOME", "CACHEDB"):
if key in os.environ:
lenv[key] = os.environ[key]
rpath = []
arch_ldflags = []
def tici_libpath(path: str) -> str:
tici_sysroot = os.environ.get("SP_TICI_SYSROOT", "").strip().rstrip("/")
if arch != "larch64" or not tici_sysroot or not path.startswith("/"):
return path
return os.path.join(tici_sysroot, path.lstrip("/"))
if arch == "larch64":
cpppath = [
"#third_party/opencl/include",
tici_libpath("/usr/local/include"),
tici_libpath("/usr/include"),
tici_libpath("/usr/include/aarch64-linux-gnu"),
]
libpath = [
tici_libpath("/usr/local/lib"),
tici_libpath("/system/vendor/lib64"),
f"#third_party/acados/{arch}/lib",
]
libpath += [
"#third_party/libyuv/larch64/lib",
tici_libpath("/lib/aarch64-linux-gnu"),
tici_libpath("/usr/lib/aarch64-linux-gnu")
]
cflags = ["-D__TICI__", "-DQCOM2", "-mcpu=cortex-a57"]
cxxflags = ["-D__TICI__", "-DQCOM2", "-mcpu=cortex-a57"]
arch_ldflags += [
f"-Wl,-rpath-link,{tici_libpath('/usr/local/lib')}",
f"-Wl,-rpath-link,{tici_libpath('/lib/aarch64-linux-gnu')}",
f"-Wl,-rpath-link,{tici_libpath('/usr/lib/aarch64-linux-gnu')}",
f"-Wl,-rpath-link,{tici_libpath('/system/vendor/lib64')}",
f"-Wl,-rpath-link,{tici_libpath('/vendor/lib64')}",
]
# On non-aarch64 hosts (e.g. Docker on macOS), force clang cross-targeting.
if platform.machine() not in ("aarch64", "arm64"):
cross_target = os.environ.get("SP_CROSS_TARGET", "aarch64-linux-gnu")
cflags += [f"--target={cross_target}"]
cxxflags += [f"--target={cross_target}"]
arch_ldflags += [f"--target={cross_target}"]
rpath += ["/usr/local/lib"]
else:
cflags = []
cxxflags = []
cpppath = []
rpath += []
# MacOS
if arch == "Darwin":
libpath = [
f"#third_party/libyuv/{arch}/lib",
f"#third_party/acados/{arch}/lib",
f"{brew_prefix}/lib",
f"{brew_prefix}/opt/openssl@3.0/lib",
"/System/Library/Frameworks/OpenGL.framework/Libraries",
]
cflags += ["-DGL_SILENCE_DEPRECATION"]
cxxflags += ["-DGL_SILENCE_DEPRECATION"]
cpppath += [
f"{brew_prefix}/include",
f"{brew_prefix}/opt/openssl@3.0/include",
]
# Linux
else:
libpath = [
f"#third_party/acados/{arch}/lib",
f"#third_party/libyuv/{arch}/lib",
"/usr/lib",
"/usr/local/lib",
]
if GetOption('asan'):
ccflags = ["-fsanitize=address", "-fno-omit-frame-pointer"]
ldflags = ["-fsanitize=address"]
elif GetOption('ubsan'):
ccflags = ["-fsanitize=undefined"]
ldflags = ["-fsanitize=undefined"]
else:
ccflags = []
ldflags = []
ldflags += arch_ldflags
# AGNOS devices are memory-constrained during on-device C++ compiles.
# Building without debug symbols dramatically reduces peak clang memory and
# prevents lowmemorykiller SIGKILLs (Error -9) on large translation units.
use_debug_symbols = os.environ.get("SP_FORCE_DEBUG_SYMBOLS", "").lower() in {"1", "true", "yes", "on"}
debug_flag = "-g" if (not AGNOS or use_debug_symbols) else "-g0"
# no --as-needed on mac linker
if arch != "Darwin":
ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"]
ccflags_option = GetOption('ccflags')
if ccflags_option:
ccflags += ccflags_option.split(' ')
env = Environment( env = Environment(
ENV=lenv, ENV={
"PATH": os.environ['PATH'],
"PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath,
"ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath,
"ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath,
"TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer"
},
CC='clang',
CXX='clang++',
CCFLAGS=[ CCFLAGS=[
debug_flag, "-g",
"-fPIC", "-fPIC",
"-O2", "-O2",
"-Wunused", "-Wunused",
@@ -271,38 +61,31 @@ env = Environment(
"-Wno-c99-designator", "-Wno-c99-designator",
"-Wno-reorder-init-list", "-Wno-reorder-init-list",
"-Wno-vla-cxx-extension", "-Wno-vla-cxx-extension",
] + cflags + ccflags, ],
CFLAGS=["-std=gnu11"],
CPPPATH=cpppath + [ CXXFLAGS=["-std=c++1z"],
CPPPATH=[
"#", "#",
"#msgq",
"#third_party",
"#third_party/json11",
"#third_party/linux/include",
"#third_party/acados/include", "#third_party/acados/include",
"#third_party/acados/include/blasfeo/include", "#third_party/acados/include/blasfeo/include",
"#third_party/acados/include/hpipm/include", "#third_party/acados/include/hpipm/include",
"#third_party/catch2/include", "#third_party/catch2/include",
"#third_party/libyuv/include", "#third_party/libyuv/include",
"#third_party/json11",
"#third_party/linux/include",
"#third_party",
"#msgq",
], ],
LIBPATH=[
CC=cc, "#common",
CXX=cxx,
AR=ar,
RANLIB=ranlib,
LINKFLAGS=ldflags,
RPATH=rpath,
CFLAGS=["-std=gnu11"] + cflags,
CXXFLAGS=["-std=c++1z"] + cxxflags,
LIBPATH=libpath + [
"#msgq_repo", "#msgq_repo",
"#third_party", "#third_party",
"#selfdrive/pandad", "#selfdrive/pandad",
"#common",
"#rednose/helpers", "#rednose/helpers",
f"#third_party/libyuv/{arch}/lib",
f"#third_party/acados/{arch}/lib",
], ],
RPATH=[],
CYTHONCFILESUFFIX=".cpp", CYTHONCFILESUFFIX=".cpp",
COMPILATIONDB_USE_ABSPATH=True, COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#", REDNOSE_ROOT="#",
@@ -310,36 +93,64 @@ env = Environment(
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
) )
if arch == "Darwin": # Arch-specific flags and paths
# RPATH is not supported on macOS, instead use the linker flags if arch == "larch64":
darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]] env.Append(CPPPATH=["#third_party/opencl/include"])
env["LINKFLAGS"] += darwin_rpath_link_flags env.Append(LIBPATH=[
"/usr/local/lib",
"/system/vendor/lib64",
"/usr/lib/aarch64-linux-gnu",
])
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57"]
env.Append(CCFLAGS=arch_flags)
env.Append(CXXFLAGS=arch_flags)
elif arch == "Darwin":
env.Append(LIBPATH=[
f"{brew_prefix}/lib",
f"{brew_prefix}/opt/openssl@3.0/lib",
f"{brew_prefix}/opt/llvm/lib/c++",
"/System/Library/Frameworks/OpenGL.framework/Libraries",
])
env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CPPPATH=[
f"{brew_prefix}/include",
f"{brew_prefix}/opt/openssl@3.0/include",
])
else:
env.Append(LIBPATH=[
"/usr/lib",
"/usr/local/lib",
])
env.CompilationDatabase('compile_commands.json') # Sanitizers and extra CCFLAGS from CLI
if GetOption('asan'):
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])
env.Append(LINKFLAGS=["-fsanitize=address"])
elif GetOption('ubsan'):
env.Append(CCFLAGS=["-fsanitize=undefined"])
env.Append(LINKFLAGS=["-fsanitize=undefined"])
# Setup cache dir _extra_cc = shlex.split(GetOption('ccflags') or '')
cache_dir = os.environ.get("SP_SCONS_CACHE_DIR", "").strip() if _extra_cc:
if not cache_dir: env.Append(CCFLAGS=_extra_cc)
cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache'
CacheDir(cache_dir)
Clean(["."], cache_dir)
# no --as-needed on mac linker
if arch != "Darwin":
env.Append(LINKFLAGS=["-Wl,--as-needed", "-Wl,--no-undefined"])
# progress output
node_interval = 5 node_interval = 5
node_count = 0 node_count = 0
def progress_function(node): def progress_function(node):
global node_count global node_count
node_count += node_interval node_count += node_interval
sys.stderr.write("progress: %d\n" % node_count) sys.stderr.write("progress: %d\n" % node_count)
if os.environ.get('SCONS_PROGRESS'): if os.environ.get('SCONS_PROGRESS'):
Progress(progress_function, interval=node_interval) Progress(progress_function, interval=node_interval)
# Cython build environment # ********** Cython build environment **********
py_include = sysconfig.get_paths()['include'] py_include = sysconfig.get_paths()['include']
if arch == "larch64" and platform.machine() not in ("aarch64", "arm64"):
tici_py_include = tici_libpath(f"/usr/include/python{sys.version_info.major}.{sys.version_info.minor}")
if os.path.isdir(tici_py_include):
py_include = tici_py_include
envCython = env.Clone() envCython = env.Clone()
envCython["CPPPATH"] += [py_include, np.get_include()] envCython["CPPPATH"] += [py_include, np.get_include()]
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-shadow", "-Wno-deprecated-declarations"] envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-shadow", "-Wno-deprecated-declarations"]
@@ -347,124 +158,27 @@ envCython["CCFLAGS"].remove("-Werror")
envCython["LIBS"] = [] envCython["LIBS"] = []
if arch == "Darwin": if arch == "Darwin":
envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"] + darwin_rpath_link_flags envCython["LINKFLAGS"] = env["LINKFLAGS"] + ["-bundle", "-undefined", "dynamic_lookup"]
else: else:
envCython["LINKFLAGS"] = arch_ldflags + ["-pthread", "-shared"] envCython["LINKFLAGS"] = ["-pthread", "-shared"]
np_version = SCons.Script.Value(np.__version__) np_version = SCons.Script.Value(np.__version__)
Export('envCython', 'np_version') Export('envCython', 'np_version')
# Qt build environment Export('env', 'arch')
qt_env = env.Clone()
qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"]
qt_libs = [] # Setup cache dir
if arch == "Darwin": cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5" CacheDir(cache_dir)
qt_dirs = [ Clean(["."], cache_dir)
os.path.join(qt_env['QTDIR'], "include"),
]
qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules]
qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")]
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"]
qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin"))
else:
if arch == "larch64":
qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath)
# For laptop/device builds that mount an AGNOS sysroot, always prefer Qt
# headers from that sysroot to keep headers/libs ABI-matched (Qt 5.12.x).
if arch == "larch64" and os.environ.get("SP_TICI_SYSROOT"):
qt_install_prefix = tici_libpath("/usr")
qt_install_headers = tici_libpath("/usr/include/aarch64-linux-gnu/qt5")
else:
qmake = os.environ.get("SP_QMAKE", "qmake")
qt_install_prefix = subprocess.check_output([qmake, '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip()
qt_install_headers = subprocess.check_output([qmake, '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip()
qt_env['QTDIR'] = qt_install_prefix # ********** start building stuff **********
qt_dirs = [
f"{qt_install_headers}",
]
qt_gui_path = os.path.join(qt_install_headers, "QtGui")
qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))]
qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else []
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
qt_libs = [f"Qt5{m}" for m in qt_modules]
if arch == "larch64":
qt_libs += ["GLESv2", "wayland-client"]
elif arch != "Darwin":
qt_libs += ["GL"]
qt_env['QT3DIR'] = qt_env['QTDIR']
qt_env.Tool('qt3')
if arch == "larch64" and os.environ.get("SP_TICI_SYSROOT"):
qt_tool_bin = tici_libpath("/usr/lib/qt5/bin")
qt_tool_root = tici_libpath("/")
qt_arm_moc = os.path.join(qt_tool_bin, "moc")
qt_arm_uic = os.path.join(qt_tool_bin, "uic")
qt_arm_rcc = os.path.join(qt_tool_bin, "rcc")
if platform.machine() in ("aarch64", "arm64"):
if os.path.isfile(qt_arm_moc):
qt_env['QT3_MOC'] = qt_arm_moc
if os.path.isfile(qt_arm_uic):
qt_env['QT3_UIC'] = qt_arm_uic
if os.path.isfile(qt_arm_rcc):
qt_env['SP_QT_RCC'] = qt_arm_rcc
else:
qt_qemu = shutil.which("qemu-aarch64-static") or shutil.which("qemu-aarch64")
if qt_qemu and os.path.isfile(qt_arm_moc):
qt_env['QT3_MOC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_moc}"
else:
qt_host_bin = os.environ.get("SP_QT_HOST_BIN", "/usr/lib/qt5/bin")
qt_env['QT3_MOC'] = os.environ.get("SP_QT_HOST_MOC", os.path.join(qt_host_bin, "moc"))
if qt_qemu and os.path.isfile(qt_arm_uic):
qt_env['QT3_UIC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_uic}"
else:
qt_host_bin = os.environ.get("SP_QT_HOST_BIN", "/usr/lib/qt5/bin")
qt_env['QT3_UIC'] = os.environ.get("SP_QT_HOST_UIC", os.path.join(qt_host_bin, "uic"))
if qt_qemu and os.path.isfile(qt_arm_rcc):
qt_env['SP_QT_RCC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_rcc}"
else:
qt_env['SP_QT_RCC'] = os.environ.get("SP_QT_HOST_RCC", "rcc")
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"]
qt_flags = [
"-D_REENTRANT",
"-DQT_NO_DEBUG",
"-DQT_WIDGETS_LIB",
"-DQT_GUI_LIB",
"-DQT_CORE_LIB",
"-DQT_MESSAGELOGCONTEXT",
]
qt_env['CXXFLAGS'] += qt_flags
qt_env['LIBPATH'] += ['#selfdrive/ui', ]
qt_env['LIBS'] = qt_libs
if GetOption("clazy"):
checks = [
"level0",
"level1",
"no-range-loop",
"no-non-pod-global-static",
]
qt_env['CXX'] = 'clazy'
qt_env['ENV']['CLAZY_IGNORE_DIRS'] = qt_dirs[0]
qt_env['ENV']['CLAZY_CHECKS'] = ','.join(checks)
Export('env', 'qt_env', 'arch', 'real_arch')
# Build common module # Build common module
SConscript(['common/SConscript']) SConscript(['common/SConscript'])
Import('_common', '_gpucommon') Import('_common')
common = [_common, 'json11', 'zmq'] common = [_common, 'json11', 'zmq']
gpucommon = [_gpucommon] Export('common')
Export('common', 'gpucommon')
# Build messaging (cereal + msgq + socketmaster + their dependencies) # Build messaging (cereal + msgq + socketmaster + their dependencies)
# Enable swaglog include in submodules # Enable swaglog include in submodules
@@ -482,6 +196,7 @@ Export('messaging')
# Build other submodules # Build other submodules
SConscript(['panda/SConscript']) SConscript(['panda/SConscript'])
SConscript(['panda_tici/SConscript'])
# Build rednose library # Build rednose library
SConscript(['rednose/SConscript']) SConscript(['rednose/SConscript'])
@@ -505,6 +220,5 @@ if Dir('#tools/cabana/').exists() and GetOption('extras'):
if arch != "larch64": if arch != "larch64":
SConscript(['tools/cabana/SConscript']) SConscript(['tools/cabana/SConscript'])
external_sconscript = GetOption('external_sconscript')
if external_sconscript: env.CompilationDatabase('compile_commands.json')
SConscript([external_sconscript])
+26
View File
@@ -0,0 +1,26 @@
# Sponsors 贊助者
我們誠摯感謝以下贊助者提供的硬體資源,讓專案能夠在多種平台上進行測試與驗證。
We sincerely thank the following sponsors for providing hardware resources, which enable the project to be tested and validated across multiple platforms.
---
## 贊助者列表 Sponsors
| 贊助者 Sponsor | 設備 Deices | 備註 Notes |
| -------------- | ----------- | ---------- |
| BlueGood | <ul><li>Radar Filter x 2</li><li>sDSU x1</li></ul> | - |
| Chia Chun Lee | <ul><li>C3 Quick Mount x1</li></ul> | - |
| CloudJ | <ul><li>C3X x1</li></ul> | - |
| FareWay | <ul><li>EON Quick Mount x1</li><li>C2/C3 Quick Mount x1</li></ul> | Special thanks for fixing my good old EON |
| Fred Wang | <ul><li>Oneplus 3t x1</li></ul> | - |
| Saber Huang | <ul><li>O3L x1</li></ul> | - |
| [馬威 Mr. One](https://shop61532546.taobao.com/) | <ul><li>O3 x 1</li><li>O3 (Dev) x1</li><li>O3XL x1</li><li>Red Panda x1</li><li>Panda Jungle v1 x1</li></ul> | - |
| 門文梁 | <ul><li>C1.5 x1</li></ul> | - |
---
🙏 沒有你們的支持,我們無法讓專案在這麼多硬體平台上持續成長與驗證。
Without your support, this project could not continue to grow and be validated across so many hardware platforms.
-2972
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/laptop_device_build.sh" build "$@"
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" c3 "$@"
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" c4 "$@"
+84 -329
View File
@@ -1,8 +1,6 @@
using Cxx = import "./include/c++.capnp"; using Cxx = import "./include/c++.capnp";
$Cxx.namespace("cereal"); $Cxx.namespace("cereal");
using Car = import "car.capnp";
@0xb526ba661d550a59; @0xb526ba661d550a59;
# custom.capnp: a home for empty structs reserved for custom forks # custom.capnp: a home for empty structs reserved for custom forks
@@ -12,239 +10,110 @@ using Car = import "car.capnp";
# DO rename the structs # DO rename the structs
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af) # DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
struct StarPilotCarControl @0x81c2f05a394cf4af { struct ControlsStateExt @0x81c2f05a394cf4af {
hudControl @0 :HUDControl; alkaActive @0 :Bool;
}
struct HUDControl { struct CarStateExt @0xaedffd8f31e7b55d {
audibleAlert @0 :AudibleAlert; # dp - ALKA: lkasOn state from carstate (mirrors panda's lkas_on)
lkasOn @0 :Bool;
}
enum AudibleAlert { struct ModelExt @0xf35cc4560bbf6ec2 {
none @0; leftEdgeDetected @0 :Bool;
rightEdgeDetected @1 :Bool;
}
engage @1; struct LiveGPS @0xda96579883444c35 {
disengage @2; # Position
refuse @3; latitude @0 :Float64; # degrees
longitude @1 :Float64; # degrees
altitude @2 :Float64; # meters (WGS84)
warningSoft @4; # Motion
warningImmediate @5; speed @3 :Float32; # m/s (horizontal speed)
bearingDeg @4 :Float32; # degrees (heading)
prompt @6; # Accuracy
promptRepeat @7; horizontalAccuracy @5 :Float32; # meters
promptDistracted @8; verticalAccuracy @6 :Float32; # meters
# Random Events # Status
angry @9; gpsOK @7 :Bool; # livePose valid + GPS fresh
continued @10; status @8 :Status;
dejaVu @11;
doc @12; enum Status {
fart @13; uninitialized @0; # no GPS data yet
firefox @14; uncalibrated @1; # has GPS but fusion not ready (raw passthrough)
goat @15; valid @2; # fusion active with calibrated bearing
hal9000 @16;
mail @17;
nessie @18;
noice @19;
startup @20;
thisIsFine @21;
uwu @22;
}
} }
# Metadata
unixTimestampMillis @9 :Int64;
lastGpsTimestamp @10 :UInt64; # logMonoTime of last GPS
# livePose health (for debugging fusion issues)
livePoseOk @11 :Bool; # livePose valid and providing orientation/velocity
} }
struct StarPilotCarParams @0xaedffd8f31e7b55d { struct MaaControl @0x80ae746ee2596b11 {
alternativeExperience @0 :Int16; # Map-Aware Assist control signals
canUsePedal @1 :Bool;
canUseSDSU @2 :Bool;
flags @3 :UInt32;
isHDA2 @4 :Bool;
openpilotLongitudinalControlDisabled @5 :Bool;
safetyConfigs @6 :List(SafetyConfig);
struct SafetyConfig { # Curvature data (for lateral control)
safetyParam @0 :UInt16; curvature @0 :Float32; # current nav curvature (1/m)
} curvatureValid @1 :Bool; # curvature data is valid
}
struct StarPilotCarState @0xf35cc4560bbf6ec2 { # Turn speed data (for longitudinal control)
accelPressed @0 :Bool; turnSpeedLimit @2 :Float32; # target speed for turn (m/s)
alwaysOnLateralEnabled @1 :Bool; turnDistance @3 :Float32; # distance to turn (m)
brakeLights @2 :Bool; turnDirection @4 :TurnDirection;
dashboardSpeedLimit @3 :Float32; turnValid @5 :Bool; # turn data is valid
decelPressed @4 :Bool; maneuverType @6 :ManeuverType; # type of maneuver (turn vs lane change)
distancePressed @5 :Bool; turnAngle @7 :Float32; # expected turn angle in degrees (positive=left, negative=right)
distanceLongPressed @6 :Bool; turnCurvature @8 :Float32; # curvature at turn point (1/m), used for speed limit calc
distanceVeryLongPressed @7 :Bool;
ecoGear @8 :Bool;
forceCoast @9 :Bool;
isParked @10 :Bool;
pauseLateral @11 :Bool;
pauseLongitudinal @12 :Bool;
sportGear @13 :Bool;
trafficModeEnabled @14 :Bool;
gasStack @15 :Bool; # Compatibility with older StarPilot payloads
}
struct StarPilotDeviceState @0xda96579883444c35 { # Turn execution (heading-based tracking)
freeSpace @0 :Int16; desireActive @9 :Bool; # true when turn desire should be sent to model
usedSpace @1 :Int16; turnState @10 :UInt8; # TurnState enum: 0=none, 1=approaching, 2=executing, 3=complete
} turnProgress @11 :Float32; # accumulated heading change during turn (degrees)
struct StarPilotModelDataV2 @0x80ae746ee2596b11 { # Driver acknowledgment (blinker = commit to turn)
turnDirection @0 :TurnDirection; driverAcknowledged @12 :Bool; # driver turned on blinker matching turn direction
speedLimitActive @13 :Bool; # turn speed limit should be enforced (blinker on)
blockLaneChange @14 :Bool; # within commit distance, block lane change desire
enum TurnDirection { enum TurnDirection {
none @0; none @0;
turnLeft @1; left @1;
turnRight @2; right @2;
}
}
struct StarPilotOnroadEvent @0xa5cd762cd951a455 {
name @0 :EventName;
enable @1 :Bool;
noEntry @2 :Bool;
warning @3 :Bool;
userDisable @4 :Bool;
softDisable @5 :Bool;
immediateDisable @6 :Bool;
preEnable @7 :Bool;
permanent @8 :Bool;
overrideLateral @9 :Bool;
overrideLongitudinal @10 :Bool;
enum EventName {
blockUser @0;
customStartupAlert @1;
forcingStop @2;
goatSteerSaturated @3;
greenLight @4;
holidayActive @5;
laneChangeBlockedLoud @6;
leadDeparting @7;
noLaneAvailable @8;
nnffLoaded @9;
openpilotCrashed @10;
pedalInterceptorNoBrake @11;
speedLimitChanged @12;
trafficModeActive @13;
trafficModeInactive @14;
turningLeft @15;
turningRight @16;
# Random Events
accel30 @17;
accel35 @18;
accel40 @19;
dejaVuCurve @20;
firefoxSteerSaturated @21;
hal9000 @22;
openpilotCrashedRandomEvent @23;
thisIsFineSteerSaturated @24;
toBeContinued @25;
vCruise69 @26;
yourFrogTriedToKillMe @27;
youveGotMail @28;
switchbackModeActive @29;
switchbackModeInactive @30;
}
}
struct StarPilotPlan @0xf98d843bfd7004a3 {
accelerationJerk @0 :Float32;
cscControllingSpeed @1 :Bool;
cscSpeed @2 :Float32;
cscTraining @3 :Bool;
dangerFactor @4 :Float32;
dangerJerk @5 :Float32;
desiredFollowDistance @6 :Int64;
experimentalMode @7 :Bool;
forcingStop @8 :Bool;
forcingStopLength @9 :Float32;
starpilotEvents @10 :List(StarPilotOnroadEvent);
starpilotToggles @11 :Text;
increasedStoppedDistance @12 :Float32;
lateralCheck @13 :Bool;
laneWidthLeft @14 :Float32;
laneWidthRight @15 :Float32;
maxAcceleration @16 :Float32;
minAcceleration @17 :Float32;
redLight @18 :Bool;
roadCurvature @19 :Float32;
slcMapSpeedLimit @20 :Float32;
slcMapboxSpeedLimit @21 :Float32;
slcNextSpeedLimit @22 :Float32;
slcOverriddenSpeed @23 :Float32;
slcSpeedLimit @24 :Float32;
slcSpeedLimitOffset @25 :Float32;
slcSpeedLimitSource @26 :Text;
speedJerk @27 :Float32;
speedLimitChanged @28 :Bool;
tFollow @29 :Float32;
themeUpdated @30 :Bool;
unconfirmedSlcSpeedLimit @31 :Float32;
vCruise @32 :Float32;
weatherDaytime @33 :Bool;
weatherId @34 :Int16;
disableThrottle @35 :Bool;
trackingLead @36 :Bool;
}
struct StarPilotRadarState @0xb86e6369214c01c8 {
leadLeft @0 :LeadData;
leadRight @1 :LeadData;
struct LeadData {
dRel @0 :Float32;
yRel @1 :Float32;
vRel @2 :Float32;
aRel @3 :Float32;
vLead @4 :Float32;
dPath @6 :Float32;
vLat @7 :Float32;
vLeadK @8 :Float32;
aLeadK @9 :Float32;
fcw @10 :Bool;
status @11 :Bool;
aLeadTau @12 :Float32;
modelProb @13 :Float32;
radar @14 :Bool;
radarTrackId @15 :Int32 = -1;
aLeadDEPRECATED @5 :Float32;
}
}
struct StarPilotSelfdriveState @0xf416ec09499d9d19 {
alertText1 @0 :Text;
alertText2 @1 :Text;
alertStatus @2 :AlertStatus;
alertSize @3 :AlertSize;
alertType @4 :Text;
alertSound @5 :Car.CarControl.HUDControl.AudibleAlert;
enum AlertStatus {
normal @0;
userPrompt @1;
critical @2;
starpilot @3;
} }
enum AlertSize { enum ManeuverType {
none @0; none @0;
small @1; turn @1; # intersection turn - use turnLeft/Right desire
mid @2; laneChange @2; # highway exit/fork - use laneChangeLeft/Right desire
full @3;
} }
} }
struct DashyState @0xa5cd762cd951a455 {
# Pre-serialized JSON bytes for dashy UI
# Aggregates all topics needed by dashy into single message
json @0 :Data;
}
struct NavInstructionExt @0xf98d843bfd7004a3 {
# Extension fields for NavInstruction (not in upstream)
turnAngle @0 :Float32; # degrees, positive=left, negative=right
turnCurvature @1 :Float32; # 1/m, positive=left, negative=right
}
struct CustomReserved7 @0xb86e6369214c01c8 {
}
struct CustomReserved8 @0xf416ec09499d9d19 {
}
struct CustomReserved9 @0xa1680744031fdb2d { struct CustomReserved9 @0xa1680744031fdb2d {
slotId @0 :Text;
slotName @1 :Text;
variant @2 :Text;
variantLabel @3 :Text;
reason @4 :Text;
wallTimeNanos @5 :UInt64;
} }
struct CustomReserved10 @0xcb9fd56c7057593a { struct CustomReserved10 @0xcb9fd56c7057593a {
@@ -268,125 +137,11 @@ struct CustomReserved15 @0xbd443b539493bc68 {
struct CustomReserved16 @0xfc6241ed8877b611 { struct CustomReserved16 @0xfc6241ed8877b611 {
} }
struct MapdDownloadLocationDetails @0xff889853e7b0987f { struct CustomReserved17 @0xa30662f84033036c {
location @0 :Text;
totalFiles @1 :UInt32;
downloadedFiles @2 :UInt32;
} }
struct MapdDownloadProgress @0xfaa35dcac85073a2 { struct CustomReserved18 @0xc86a3d38d13eb3ef {
active @0 :Bool;
cancelled @1 :Bool;
totalFiles @2 :UInt32;
downloadedFiles @3 :UInt32;
locations @4 :List(Text);
locationDetails @5 :List(MapdDownloadLocationDetails);
} }
struct MapdPathPoint @0xd6f78acca1bc3939 { struct CustomReserved19 @0xa4f1eb3323f5f582 {
latitude @0 :Float64;
longitude @1 :Float64;
curvature @2 :Float32;
targetVelocity @3 :Float32;
}
struct MapdExtendedOut @0xa30662f84033036c {
downloadProgress @0 :MapdDownloadProgress;
settings @1 :Text;
path @2 :List(MapdPathPoint);
}
enum MapdInputType {
download @0;
setTargetLateralAccel @1;
setSpeedLimitOffset @2;
setSpeedLimitControl @3;
setMapCurveSpeedControl @4;
setVisionCurveSpeedControl @5;
setLogLevel @6;
setVisionCurveTargetLatA @7;
setVisionCurveMinTargetV @8;
reloadSettings @9;
saveSettings @10;
setEnableSpeed @11;
setVisionCurveUseEnableSpeed @12;
setMapCurveUseEnableSpeed @13;
setSpeedLimitUseEnableSpeed @14;
setHoldLastSeenSpeedLimit @15;
setTargetSpeedJerk @16;
setTargetSpeedAccel @17;
setTargetSpeedTimeOffset @18;
setDefaultLaneWidth @19;
setMapCurveTargetLatA @20;
loadDefaultSettings @21;
loadRecommendedSettings @22;
setSlowDownForNextSpeedLimit @23;
setSpeedUpForNextSpeedLimit @24;
setHoldSpeedLimitWhileChangingSetSpeed @25;
loadPersistentSettings @26;
cancelDownload @27;
setLogJson @28;
setLogSource @29;
setExternalSpeedLimitControl @30;
setExternalSpeedLimit @31;
setSpeedLimitPriority @32;
setSpeedLimitChangeRequiresAccept @33;
acceptSpeedLimit @34;
setPressGasToAcceptSpeedLimit @35;
setAdjustSetSpeedToAcceptSpeedLimit @36;
setAcceptSpeedLimitTimeout @37;
setPressGasToOverrideSpeedLimit @38;
}
enum WaySelectionType {
current @0;
predicted @1;
possible @2;
extended @3;
fail @4;
}
enum SpeedLimitOffsetType {
static @0;
percent @1;
}
struct MapdIn @0xc86a3d38d13eb3ef {
type @0 :MapdInputType;
float @1 :Float32;
str @2 :Text;
bool @3 :Bool;
}
enum RoadContext {
freeway @0;
city @1;
unknown @2;
}
struct MapdOut @0xa4f1eb3323f5f582 {
wayName @0 :Text;
wayRef @1 :Text;
roadName @2 :Text;
speedLimit @3 :Float32;
nextSpeedLimit @4 :Float32;
nextSpeedLimitDistance @5 :Float32;
hazard @6 :Text;
nextHazard @7 :Text;
nextHazardDistance @8 :Float32;
advisorySpeed @9 :Float32;
nextAdvisorySpeed @10 :Float32;
nextAdvisorySpeedDistance @11 :Float32;
oneWay @12 :Bool;
lanes @13 :UInt8;
tileLoaded @14 :Bool;
speedLimitSuggestedSpeed @15 :Float32;
suggestedSpeed @16 :Float32;
estimatedRoadWidth @17 :Float32;
roadContext @18 :RoadContext;
distanceFromWayCenter @19 :Float32;
visionCurveSpeed @20 :Float32;
mapCurveSpeed @21 :Float32;
waySelectionType @22 :WaySelectionType;
speedLimitAccepted @23 :Bool;
} }
Binary file not shown.
Binary file not shown.
+18 -26
View File
@@ -130,7 +130,6 @@ struct OnroadEvent @0xc4fa6047f024e718 {
userBookmark @95; userBookmark @95;
excessiveActuation @96; excessiveActuation @96;
audioFeedback @97; audioFeedback @97;
lateralManeuver @98;
soundsUnavailableDEPRECATED @47; soundsUnavailableDEPRECATED @47;
} }
@@ -1088,7 +1087,7 @@ struct ModelDataV2 {
confidence @23: ConfidenceClass; confidence @23: ConfidenceClass;
# Model perceived motion # Model perceived motion
temporalPose @21 :Pose; temporalPoseDEPRECATED @21 :Pose;
# e2e lateral planner # e2e lateral planner
action @26: Action; action @26: Action;
@@ -1239,10 +1238,6 @@ struct DriverAssistance {
# FCW, AEB, etc. will go here # FCW, AEB, etc. will go here
} }
struct LateralManeuverPlan {
desiredCurvature @0 :Float32; # 1/m
}
struct LongitudinalPlan @0xe00b5b3eba12876c { struct LongitudinalPlan @0xe00b5b3eba12876c {
modelMonoTime @9 :UInt64; modelMonoTime @9 :UInt64;
hasLead @7 :Bool; hasLead @7 :Bool;
@@ -2166,14 +2161,12 @@ struct DriverStateV2 {
facePosition @2 :List(Float32); facePosition @2 :List(Float32);
facePositionStd @3 :List(Float32); facePositionStd @3 :List(Float32);
faceProb @4 :Float32; faceProb @4 :Float32;
eyesVisibleProb @14 :Float32; leftEyeProb @5 :Float32;
eyesClosedProb @15 :Float32; rightEyeProb @6 :Float32;
leftBlinkProb @7 :Float32;
rightBlinkProb @8 :Float32;
sunglassesProb @9 :Float32;
phoneProb @13 :Float32; phoneProb @13 :Float32;
leftEyeProbDEPRECATED @5 :Float32;
rightEyeProbDEPRECATED @6 :Float32;
leftBlinkProbDEPRECATED @7 :Float32;
rightBlinkProbDEPRECATED @8 :Float32;
sunglassesProbDEPRECATED @9 :Float32;
notReadyProbDEPRECATED @12 :List(Float32); notReadyProbDEPRECATED @12 :List(Float32);
occludedProbDEPRECATED @10 :Float32; occludedProbDEPRECATED @10 :Float32;
readyProbDEPRECATED @11 :List(Float32); readyProbDEPRECATED @11 :List(Float32);
@@ -2608,7 +2601,6 @@ struct Event {
userBookmark @93 :UserBookmark; userBookmark @93 :UserBookmark;
bookmarkButton @148 :UserBookmark; bookmarkButton @148 :UserBookmark;
audioFeedback @149 :AudioFeedback; audioFeedback @149 :AudioFeedback;
lateralManeuverPlan @150 :LateralManeuverPlan;
# *********** debug *********** # *********** debug ***********
testJoystick @52 :Joystick; testJoystick @52 :Joystick;
@@ -2633,15 +2625,15 @@ struct Event {
# DO change the name of the field and struct # DO change the name of the field and struct
# DON'T change the ID (e.g. @107) # DON'T change the ID (e.g. @107)
# DON'T change which struct it points to # DON'T change which struct it points to
starpilotCarControl @107 :Custom.StarPilotCarControl; controlsStateExt @107 :Custom.ControlsStateExt;
starpilotCarParams @108 :Custom.StarPilotCarParams; carStateExt @108 :Custom.CarStateExt;
starpilotCarState @109 :Custom.StarPilotCarState; modelExt @109 :Custom.ModelExt;
starpilotDeviceState @110 :Custom.StarPilotDeviceState; liveGPS @110 :Custom.LiveGPS;
starpilotModelV2 @111 :Custom.StarPilotModelDataV2; maaControl @111 :Custom.MaaControl;
starpilotOnroadEvents @112 :List(Custom.StarPilotOnroadEvent); dashyState @112 :Custom.DashyState;
starpilotPlan @113 :Custom.StarPilotPlan; navInstructionExt @113 :Custom.NavInstructionExt;
starpilotRadarState @114 :Custom.StarPilotRadarState; customReserved7 @114 :Custom.CustomReserved7;
starpilotSelfdriveState @115 :Custom.StarPilotSelfdriveState; customReserved8 @115 :Custom.CustomReserved8;
customReserved9 @116 :Custom.CustomReserved9; customReserved9 @116 :Custom.CustomReserved9;
customReserved10 @136 :Custom.CustomReserved10; customReserved10 @136 :Custom.CustomReserved10;
customReserved11 @137 :Custom.CustomReserved11; customReserved11 @137 :Custom.CustomReserved11;
@@ -2650,9 +2642,9 @@ struct Event {
customReserved14 @140 :Custom.CustomReserved14; customReserved14 @140 :Custom.CustomReserved14;
customReserved15 @141 :Custom.CustomReserved15; customReserved15 @141 :Custom.CustomReserved15;
customReserved16 @142 :Custom.CustomReserved16; customReserved16 @142 :Custom.CustomReserved16;
mapdExtendedOut @143 :Custom.MapdExtendedOut; customReserved17 @143 :Custom.CustomReserved17;
mapdIn @144 :Custom.MapdIn; customReserved18 @144 :Custom.CustomReserved18;
mapdOut @145 :Custom.MapdOut; customReserved19 @145 :Custom.CustomReserved19;
# *********** legacy + deprecated *********** # *********** legacy + deprecated ***********
model @9 :Legacy.ModelData; # TODO: rename modelV2 and mark this as deprecated model @9 :Legacy.ModelData; # TODO: rename modelV2 and mark this as deprecated
-23
View File
@@ -197,10 +197,6 @@ class SubMaster:
self.data[s] = getattr(data.as_reader(), s) self.data[s] = getattr(data.as_reader(), s)
self.freq_tracker[s] = FrequencyTracker(SERVICE_LIST[s].frequency, self.update_freq, s == poll) self.freq_tracker[s] = FrequencyTracker(SERVICE_LIST[s].frequency, self.update_freq, s == poll)
# StarPilot variables
self.addr = addr
self.poll = poll
def __getitem__(self, s: str) -> capnp.lib.capnp._DynamicStructReader: def __getitem__(self, s: str) -> capnp.lib.capnp._DynamicStructReader:
return self.data[s] return self.data[s]
@@ -252,18 +248,6 @@ class SubMaster:
def all_checks(self, service_list: Optional[List[str]] = None) -> bool: def all_checks(self, service_list: Optional[List[str]] = None) -> bool:
return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list) return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list)
# StarPilot variables
def extend(self, new_services: List[str]):
return SubMaster(
self.services + new_services,
poll=self.poll,
ignore_alive=self.ignore_alive,
ignore_avg_freq=self.ignore_average_freq,
ignore_valid=self.ignore_valid,
addr=self.addr,
frequency=None if self.poll is not None else self.update_freq,
)
class PubMaster: class PubMaster:
def __init__(self, services: List[str]): def __init__(self, services: List[str]):
@@ -285,10 +269,3 @@ class PubMaster:
def all_readers_updated(self, s: str) -> bool: def all_readers_updated(self, s: str) -> bool:
return self.sock[s].all_readers_updated() # type: ignore return self.sock[s].all_readers_updated() # type: ignore
# StarPilot variables
def extend(self, new_services: List[str]):
for service in new_services:
if service not in self.sock:
self.sock[service] = pub_sock(service)
return self
Binary file not shown.
-102
View File
@@ -1,102 +0,0 @@
/* THIS IS AN AUTOGENERATED FILE, PLEASE EDIT services.py */
#ifndef __SERVICES_H
#define __SERVICES_H
#include <map>
#include <string>
struct service { std::string name; bool should_log; float frequency; int decimation; size_t queue_size; };
static std::map<std::string, service> services = {
{ "gyroscope", {"gyroscope", true, 104.000000, 104, 256000}},
{ "accelerometer", {"accelerometer", true, 104.000000, 104, 256000}},
{ "magnetometer", {"magnetometer", true, 25.000000, -1, 256000}},
{ "lightSensor", {"lightSensor", true, 100.000000, 100, 256000}},
{ "temperatureSensor", {"temperatureSensor", true, 2.000000, 200, 256000}},
{ "gpsNMEA", {"gpsNMEA", true, 9.000000, -1, 256000}},
{ "deviceState", {"deviceState", true, 2.000000, 1, 256000}},
{ "touch", {"touch", true, 20.000000, 1, 256000}},
{ "can", {"can", true, 100.000000, 2053, 10485760}},
{ "controlsState", {"controlsState", true, 100.000000, 10, 2097152}},
{ "selfdriveState", {"selfdriveState", true, 100.000000, 10, 256000}},
{ "pandaStates", {"pandaStates", true, 10.000000, 1, 256000}},
{ "peripheralState", {"peripheralState", true, 2.000000, 1, 256000}},
{ "radarState", {"radarState", true, 20.000000, 5, 256000}},
{ "roadEncodeIdx", {"roadEncodeIdx", false, 20.000000, 1, 256000}},
{ "liveTracks", {"liveTracks", true, 20.000000, -1, 256000}},
{ "sendcan", {"sendcan", true, 100.000000, 139, 2097152}},
{ "logMessage", {"logMessage", true, 0.000000, -1, 256000}},
{ "errorLogMessage", {"errorLogMessage", true, 0.000000, 1, 256000}},
{ "liveCalibration", {"liveCalibration", true, 4.000000, 4, 256000}},
{ "liveTorqueParameters", {"liveTorqueParameters", true, 4.000000, 1, 256000}},
{ "liveDelay", {"liveDelay", true, 4.000000, 1, 256000}},
{ "androidLog", {"androidLog", true, 0.000000, -1, 256000}},
{ "carState", {"carState", true, 100.000000, 10, 256000}},
{ "carControl", {"carControl", true, 100.000000, 10, 256000}},
{ "carOutput", {"carOutput", true, 100.000000, 10, 256000}},
{ "longitudinalPlan", {"longitudinalPlan", true, 20.000000, 10, 256000}},
{ "lateralManeuverPlan", {"lateralManeuverPlan", true, 20.000000, -1, 256000}},
{ "driverAssistance", {"driverAssistance", true, 20.000000, 20, 256000}},
{ "procLog", {"procLog", true, 0.500000, 15, 10485760}},
{ "gpsLocationExternal", {"gpsLocationExternal", true, 10.000000, 10, 256000}},
{ "gpsLocation", {"gpsLocation", true, 1.000000, 1, 256000}},
{ "ubloxGnss", {"ubloxGnss", true, 10.000000, -1, 256000}},
{ "qcomGnss", {"qcomGnss", true, 2.000000, -1, 256000}},
{ "gnssMeasurements", {"gnssMeasurements", true, 10.000000, 10, 256000}},
{ "clocks", {"clocks", true, 0.100000, 1, 256000}},
{ "ubloxRaw", {"ubloxRaw", true, 20.000000, -1, 256000}},
{ "livePose", {"livePose", true, 20.000000, 4, 256000}},
{ "liveParameters", {"liveParameters", true, 20.000000, 5, 256000}},
{ "cameraOdometry", {"cameraOdometry", true, 20.000000, 10, 256000}},
{ "thumbnail", {"thumbnail", true, 0.016667, 1, 256000}},
{ "onroadEvents", {"onroadEvents", true, 1.000000, 1, 256000}},
{ "carParams", {"carParams", true, 0.020000, 1, 256000}},
{ "roadCameraState", {"roadCameraState", true, 20.000000, 20, 256000}},
{ "driverCameraState", {"driverCameraState", true, 20.000000, 20, 256000}},
{ "driverEncodeIdx", {"driverEncodeIdx", false, 20.000000, 1, 256000}},
{ "driverStateV2", {"driverStateV2", true, 20.000000, 10, 256000}},
{ "driverMonitoringState", {"driverMonitoringState", true, 20.000000, 10, 256000}},
{ "wideRoadEncodeIdx", {"wideRoadEncodeIdx", false, 20.000000, 1, 256000}},
{ "wideRoadCameraState", {"wideRoadCameraState", true, 20.000000, 20, 256000}},
{ "drivingModelData", {"drivingModelData", true, 20.000000, 10, 256000}},
{ "modelV2", {"modelV2", true, 20.000000, -1, 10485760}},
{ "managerState", {"managerState", true, 2.000000, 1, 256000}},
{ "uploaderState", {"uploaderState", true, 0.000000, 1, 256000}},
{ "navInstruction", {"navInstruction", true, 1.000000, 10, 256000}},
{ "navRoute", {"navRoute", true, 0.000000, -1, 256000}},
{ "navThumbnail", {"navThumbnail", true, 0.000000, -1, 256000}},
{ "qRoadEncodeIdx", {"qRoadEncodeIdx", false, 20.000000, -1, 256000}},
{ "userBookmark", {"userBookmark", true, 0.000000, 1, 256000}},
{ "soundPressure", {"soundPressure", true, 10.000000, 10, 256000}},
{ "rawAudioData", {"rawAudioData", false, 20.000000, -1, 256000}},
{ "bookmarkButton", {"bookmarkButton", true, 0.000000, 1, 256000}},
{ "audioFeedback", {"audioFeedback", true, 0.000000, 1, 256000}},
{ "roadEncodeData", {"roadEncodeData", false, 20.000000, -1, 10485760}},
{ "driverEncodeData", {"driverEncodeData", false, 20.000000, -1, 10485760}},
{ "wideRoadEncodeData", {"wideRoadEncodeData", false, 20.000000, -1, 10485760}},
{ "qRoadEncodeData", {"qRoadEncodeData", false, 20.000000, -1, 10485760}},
{ "uiDebug", {"uiDebug", true, 0.000000, 1, 256000}},
{ "testJoystick", {"testJoystick", true, 0.000000, -1, 256000}},
{ "alertDebug", {"alertDebug", true, 20.000000, 5, 256000}},
{ "livestreamWideRoadEncodeIdx", {"livestreamWideRoadEncodeIdx", false, 20.000000, -1, 256000}},
{ "livestreamRoadEncodeIdx", {"livestreamRoadEncodeIdx", false, 20.000000, -1, 256000}},
{ "livestreamDriverEncodeIdx", {"livestreamDriverEncodeIdx", false, 20.000000, -1, 256000}},
{ "livestreamWideRoadEncodeData", {"livestreamWideRoadEncodeData", false, 20.000000, -1, 2097152}},
{ "livestreamRoadEncodeData", {"livestreamRoadEncodeData", false, 20.000000, -1, 2097152}},
{ "livestreamDriverEncodeData", {"livestreamDriverEncodeData", false, 20.000000, -1, 2097152}},
{ "customReserved9", {"customReserved9", true, 0.000000, 1, 256000}},
{ "customReservedRawData0", {"customReservedRawData0", true, 0.000000, -1, 256000}},
{ "customReservedRawData1", {"customReservedRawData1", true, 0.000000, -1, 256000}},
{ "customReservedRawData2", {"customReservedRawData2", true, 0.000000, -1, 256000}},
{ "starpilotCarControl", {"starpilotCarControl", true, 100.000000, 10, 256000}},
{ "starpilotCarParams", {"starpilotCarParams", true, 0.020000, 1, 256000}},
{ "starpilotCarState", {"starpilotCarState", true, 100.000000, 10, 256000}},
{ "starpilotDeviceState", {"starpilotDeviceState", true, 2.000000, 1, 256000}},
{ "starpilotModelV2", {"starpilotModelV2", true, 20.000000, -1, 256000}},
{ "starpilotOnroadEvents", {"starpilotOnroadEvents", true, 1.000000, 1, 256000}},
{ "starpilotPlan", {"starpilotPlan", true, 20.000000, 10, 256000}},
{ "starpilotRadarState", {"starpilotRadarState", true, 20.000000, 5, 256000}},
{ "starpilotSelfdriveState", {"starpilotSelfdriveState", true, 100.000000, 10, 256000}},
{ "mapdExtendedOut", {"mapdExtendedOut", true, 1.000000, 1, 2097152}},
{ "mapdIn", {"mapdIn", true, 1.000000, 1, 2097152}},
{ "mapdOut", {"mapdOut", true, 20.000000, 20, 2097152}},
};
#endif
+10 -17
View File
@@ -49,7 +49,6 @@ _services: dict[str, tuple] = {
"carControl": (True, 100., 10), "carControl": (True, 100., 10),
"carOutput": (True, 100., 10), "carOutput": (True, 100., 10),
"longitudinalPlan": (True, 20., 10), "longitudinalPlan": (True, 20., 10),
"lateralManeuverPlan": (True, 20.),
"driverAssistance": (True, 20., 20), "driverAssistance": (True, 20., 20),
"procLog": (True, 0.5, 15, QueueSize.BIG), "procLog": (True, 0.5, 15, QueueSize.BIG),
"gpsLocationExternal": (True, 10., 10), "gpsLocationExternal": (True, 10., 10),
@@ -76,7 +75,7 @@ _services: dict[str, tuple] = {
"modelV2": (True, 20., None, QueueSize.BIG), "modelV2": (True, 20., None, QueueSize.BIG),
"managerState": (True, 2., 1), "managerState": (True, 2., 1),
"uploaderState": (True, 0., 1), "uploaderState": (True, 0., 1),
"navInstruction": (True, 1., 10), # "navInstruction": (True, 1., 10), # dp - make it 0 hz
"navRoute": (True, 0.), "navRoute": (True, 0.),
"navThumbnail": (True, 0.), "navThumbnail": (True, 0.),
"qRoadEncodeIdx": (False, 20.), "qRoadEncodeIdx": (False, 20.),
@@ -100,24 +99,18 @@ _services: dict[str, tuple] = {
"livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), "livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), "livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM), "livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
"customReserved9": (True, 0., 1),
"customReservedRawData0": (True, 0.), "customReservedRawData0": (True, 0.),
"customReservedRawData1": (True, 0.), "customReservedRawData1": (True, 0.),
"customReservedRawData2": (True, 0.), "customReservedRawData2": (True, 0.),
"controlsStateExt": (True, 100.),
# StarPilot variables "carStateExt": (True, 100.),
"starpilotCarControl": (True, 100., 10), "modelExt": (True, 20.),
"starpilotCarParams": (True, 0.02, 1), # dashy
"starpilotCarState": (True, 100., 10), "navInstruction": (True, 0.),
"starpilotDeviceState": (True, 2., 1), "navInstructionExt": (True, 0.),
"starpilotModelV2": (True, 20.), "liveGPS": (True, 0.), # GPS fusion from gpsd (optional)
"starpilotOnroadEvents": (True, 1., 1), "maaControl": (True, 0.), # Map-Aware Assist control signals (optional)
"starpilotPlan": (True, 20., 10), "dashyState": (True, 0.), # Aggregated dashy UI state (optional)
"starpilotRadarState": (True, 20., 5),
"starpilotSelfdriveState": (True, 100., 10),
"mapdExtendedOut": (True, 1., 1, QueueSize.MEDIUM),
"mapdIn": (True, 1., 1, QueueSize.MEDIUM),
"mapdOut": (True, 20., 20, QueueSize.MEDIUM),
} }
SERVICE_LIST = {name: Service(*vals) for SERVICE_LIST = {name: Service(*vals) for
idx, (name, vals) in enumerate(_services.items())} idx, (name, vals) in enumerate(_services.items())}
-1
View File
@@ -1,2 +1 @@
*.cpp *.cpp
!params_pyx.cpp
+3 -9
View File
@@ -4,18 +4,12 @@ common_libs = [
'params.cc', 'params.cc',
'swaglog.cc', 'swaglog.cc',
'util.cc', 'util.cc',
'watchdog.cc', 'ratekeeper.cc',
'ratekeeper.cc'
]
_common = env.Library('common', common_libs, LIBS="json11")
files = [
'clutil.cc', 'clutil.cc',
] ]
_gpucommon = env.Library('gpucommon', files) _common = env.Library('common', common_libs, LIBS="json11")
Export('_common', '_gpucommon') Export('_common')
if GetOption('extras'): if GetOption('extras'):
env.Program('tests/test_common', env.Program('tests/test_common',
+1 -3
View File
@@ -5,9 +5,7 @@ from datetime import datetime, timedelta, UTC
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version from openpilot.system.version import get_version
from openpilot.starpilot.common.starpilot_utilities import use_konik_server API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
API_HOST = os.getenv('API_HOST', f"https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}")
# name: jwt signature algorithm # name: jwt signature algorithm
KEYS = {"id_rsa": "RS256", KEYS = {"id_rsa": "RS256",
+1 -32
View File
@@ -53,37 +53,6 @@ void cl_print_build_errors(cl_program program, cl_device_id device) {
LOGE("build failed; status=%d, log: %s", status, log.c_str()); LOGE("build failed; status=%d, log: %s", status, log.c_str());
} }
std::string resolve_program_path(const char *path) {
std::string resolved_path(path);
if (util::file_exists(resolved_path)) {
return resolved_path;
}
const std::string basedir = util::getenv("BASEDIR", "");
if (!basedir.empty()) {
// Handle repository-relative paths, e.g. "selfdrive/modeld/transforms/transform.cl".
if (!resolved_path.empty() && resolved_path.front() != '/') {
const std::string candidate = basedir + "/" + resolved_path;
if (util::file_exists(candidate)) {
return candidate;
}
}
// Recover from build-path-embedded absolute paths like "/work/selfdrive/...".
for (const char *marker : {"/selfdrive/", "/starpilot/", "/common/"}) {
if (const size_t idx = resolved_path.find(marker); idx != std::string::npos) {
const std::string candidate = basedir + "/" + resolved_path.substr(idx + 1);
if (util::file_exists(candidate)) {
LOGW("OpenCL source path remapped: %s -> %s", resolved_path.c_str(), candidate.c_str());
return candidate;
}
}
}
}
return resolved_path;
}
} // namespace } // namespace
cl_device_id cl_get_device_id(cl_device_type device_type) { cl_device_id cl_get_device_id(cl_device_type device_type) {
@@ -115,7 +84,7 @@ void cl_release_context(cl_context context) {
} }
cl_program cl_program_from_file(cl_context ctx, cl_device_id device_id, const char* path, const char* args) { cl_program cl_program_from_file(cl_context ctx, cl_device_id device_id, const char* path, const char* args) {
return cl_program_from_source(ctx, device_id, util::read_file(resolve_program_path(path)), args); return cl_program_from_source(ctx, device_id, util::read_file(path), args);
} }
cl_program cl_program_from_source(cl_context ctx, cl_device_id device_id, const std::string& src, const char* args) { cl_program cl_program_from_source(cl_context ctx, cl_device_id device_id, const std::string& src, const char* args) {
-6
View File
@@ -19,11 +19,5 @@ class CV:
# Mass # Mass
LB_TO_KG = 0.453592 LB_TO_KG = 0.453592
# StarPilot variables
METER_TO_FOOT = 3.28084
FOOT_TO_METER = 1. / METER_TO_FOOT
CM_TO_INCH = 1. / 2.54
INCH_TO_CM = 1. / CM_TO_INCH
ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2 ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2
-41
View File
@@ -1,41 +0,0 @@
import math
import os
from pathlib import Path
CHUNK_SIZE = 45 * 1024 * 1024 # 45MB, under GitHub's 50MB limit
def get_chunk_name(name, idx, num_chunks):
return f"{name}.chunk{idx + 1:02d}of{num_chunks:02d}"
def get_manifest_path(name):
return f"{name}.chunkmanifest"
def get_chunk_paths(path, file_size):
num_chunks = math.ceil(file_size / CHUNK_SIZE)
return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
def chunk_file(path, targets):
manifest_path, *chunk_paths = targets
with open(path, 'rb') as f:
data = f.read()
actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE))
assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}"
for i, chunk_path in enumerate(chunk_paths):
with open(chunk_path, 'wb') as f:
f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE])
Path(manifest_path).write_text(str(len(chunk_paths)))
os.remove(path)
def read_file_chunked(path):
manifest_path = get_manifest_path(path)
if os.path.isfile(manifest_path):
num_chunks = int(Path(manifest_path).read_text().strip())
return b''.join(Path(get_chunk_name(path, i, num_chunks)).read_bytes() for i in range(num_chunks))
if os.path.isfile(path):
return Path(path).read_bytes()
raise FileNotFoundError(path)
Binary file not shown.
+9 -85
View File
@@ -91,28 +91,16 @@ private:
} // namespace } // namespace
Params::Params(const std::string &path, bool memory) { Params::Params(const std::string &path) {
params_prefix = "/" + util::getenv("OPENPILOT_PREFIX", "d"); params_prefix = "/" + util::getenv("OPENPILOT_PREFIX", "d");
params_path = ensure_params_path(params_prefix, path);
// StarPilot variables
std::string params_folder;
if (memory) {
params_folder = Path::shm_path() + "/params";
} else {
cache_path = "/cache/params" + params_prefix + "/";
params_folder = path;
}
params_path = ensure_params_path(params_prefix, params_folder);
} }
Params::~Params() { Params::~Params() {
if (future.valid()) { if (future.valid()) {
future.wait(); future.wait();
} }
std::scoped_lock lk(pending_writes_lock);
assert(queue.empty()); assert(queue.empty());
assert(pending_writes.empty());
assert(!writer_running);
} }
std::vector<std::string> Params::allKeys() const { std::vector<std::string> Params::allKeys() const {
@@ -181,12 +169,6 @@ int Params::put(const char* key, const char* value, size_t value_size) {
int Params::remove(const std::string &key) { int Params::remove(const std::string &key) {
FileLock file_lock(params_path + "/.lock"); FileLock file_lock(params_path + "/.lock");
int result = unlink(getParamPath(key).c_str()); int result = unlink(getParamPath(key).c_str());
// StarPilot variables
if (!cache_path.empty()) {
unlink((cache_path + key).c_str());
}
if (result != 0) { if (result != 0) {
return result; return result;
} }
@@ -233,11 +215,6 @@ void Params::clearAll(ParamKeyFlag key_flag) {
auto it = keys.find(de->d_name); auto it = keys.find(de->d_name);
if (it == keys.end() || (it->second.flags & key_flag)) { if (it == keys.end() || (it->second.flags & key_flag)) {
unlink(getParamPath(de->d_name).c_str()); unlink(getParamPath(de->d_name).c_str());
// StarPilot variables
if (!cache_path.empty()) {
unlink((cache_path + de->d_name).c_str());
}
} }
} }
} }
@@ -248,71 +225,18 @@ void Params::clearAll(ParamKeyFlag key_flag) {
} }
void Params::putNonBlocking(const std::string &key, const std::string &val) { void Params::putNonBlocking(const std::string &key, const std::string &val) {
bool should_enqueue = false; queue.push(std::make_pair(key, val));
bool should_start_thread = false; // start thread on demand
if (!future.valid() || future.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) {
{
std::scoped_lock lk(pending_writes_lock);
auto it = pending_writes.find(key);
if (it == pending_writes.end()) {
pending_writes.emplace(key, val);
should_enqueue = true;
} else {
it->second = val;
}
if (!writer_running) {
writer_running = true;
should_start_thread = true;
}
}
if (should_enqueue) {
queue.push(key);
}
if (should_start_thread) {
future = std::async(std::launch::async, &Params::asyncWriteThread, this); future = std::async(std::launch::async, &Params::asyncWriteThread, this);
} }
} }
void Params::asyncWriteThread() { void Params::asyncWriteThread() {
std::string key; // TODO: write the latest one if a key has multiple values in the queue.
while (true) { std::pair<std::string, std::string> p;
if (!queue.try_pop(key, 0)) { while (queue.try_pop(p, 0)) {
std::scoped_lock lk(pending_writes_lock);
if (queue.empty() && pending_writes.empty()) {
writer_running = false;
return;
}
continue;
}
std::string val;
{
std::scoped_lock lk(pending_writes_lock);
auto it = pending_writes.find(key);
if (it == pending_writes.end()) {
continue;
}
val = std::move(it->second);
pending_writes.erase(it);
}
// Params::put is Thread-Safe // Params::put is Thread-Safe
put(key, val); put(p.first, p.second);
} }
} }
// StarPilot variables
int Params::getTuningLevel(const std::string &key) {
return keys[key].tuning_level;
}
std::optional<std::string> Params::getStockValue(const std::string &key) {
ParamKeyAttributes &attributes = keys[key];
if (attributes.stock_value) {
return attributes.stock_value;
}
return attributes.default_value;
}
+2 -44
View File
@@ -2,11 +2,9 @@
#include <future> #include <future>
#include <map> #include <map>
#include <mutex>
#include <optional> #include <optional>
#include <string> #include <string>
#include <tuple> #include <tuple>
#include <unordered_map>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -37,16 +35,11 @@ struct ParamKeyAttributes {
uint32_t flags; uint32_t flags;
ParamKeyType type; ParamKeyType type;
std::optional<std::string> default_value = std::nullopt; std::optional<std::string> default_value = std::nullopt;
// StarPilot variables
std::optional<std::string> stock_value = std::nullopt;
int tuning_level = 0;
}; };
class Params { class Params {
public: public:
explicit Params(const std::string &path = {}, bool memory = false); explicit Params(const std::string &path = {});
~Params(); ~Params();
// Not copyable. // Not copyable.
Params(const Params&) = delete; Params(const Params&) = delete;
@@ -85,35 +78,6 @@ public:
putNonBlocking(key, val ? "1" : "0"); putNonBlocking(key, val ? "1" : "0");
} }
// StarPilot variables
int getInt(const std::string &key, bool block = false) {
std::string value = get(key, block);
return value.empty() ? 0 : std::stoi(value);
}
float getFloat(const std::string &key, bool block = false) {
std::string value = get(key, block);
return value.empty() ? 0.0f : std::stof(value);
}
int putInt(const std::string &key, int val) {
std::string str = std::to_string(val);
return put(key.c_str(), str.c_str(), str.size());
}
int putFloat(const std::string &key, float val) {
std::string str = std::to_string(val);
return put(key.c_str(), str.c_str(), str.size());
}
void putIntNonBlocking(const std::string &key, int val) {
putNonBlocking(key, std::to_string(val));
}
void putFloatNonBlocking(const std::string &key, float val) {
putNonBlocking(key, std::to_string(val));
}
int getTuningLevel(const std::string &key);
std::optional<std::string> getStockValue(const std::string &key);
private: private:
void asyncWriteThread(); void asyncWriteThread();
@@ -122,11 +86,5 @@ private:
// for nonblocking write // for nonblocking write
std::future<void> future; std::future<void> future;
SafeQueue<std::string> queue; SafeQueue<std::pair<std::string, std::string>> queue;
std::mutex pending_writes_lock;
std::unordered_map<std::string, std::string> pending_writes;
bool writer_running = false;
// StarPilot variables
std::string cache_path;
}; };
+2 -64
View File
@@ -1,71 +1,9 @@
from openpilot.common.params_pyx import Params as _Params, ParamKeyFlag, ParamKeyType, UnknownKeyName from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName
assert _Params assert Params
assert ParamKeyFlag assert ParamKeyFlag
assert ParamKeyType assert ParamKeyType
assert UnknownKeyName assert UnknownKeyName
class Params(_Params):
def get(self, key, block=False, return_default=False, encoding=None, default=None):
try:
value = super().get(key, block=block, return_default=return_default)
except UnknownKeyName:
return default
if value is None:
return default
if encoding is not None and isinstance(value, bytes):
try:
return value.decode(encoding)
except Exception:
return value.decode("utf-8", errors="replace")
return value
def get_bool(self, key, block=False, default=False):
try:
return super().get_bool(key, block=block)
except UnknownKeyName:
return bool(default)
def get_int(self, key, block=False, return_default=False, default=0):
val = self.get(key, block=block, return_default=return_default, encoding="utf-8")
if val is None or val == "":
return default
try:
return int(float(val))
except ValueError:
return default
def get_float(self, key, block=False, return_default=False, default=0.0):
val = self.get(key, block=block, return_default=return_default, encoding="utf-8")
if val is None or val == "":
return default
try:
return float(val)
except ValueError:
return default
def put_int(self, key, val):
t = self.get_type(key)
if t == ParamKeyType.FLOAT:
self.put(key, float(val))
elif t == ParamKeyType.INT:
self.put(key, int(val))
elif t == ParamKeyType.BOOL:
self.put(key, bool(val))
else:
self.put(key, str(int(val)))
def put_float(self, key, val):
t = self.get_type(key)
if t == ParamKeyType.FLOAT:
self.put(key, float(val))
elif t == ParamKeyType.INT:
self.put(key, int(val))
elif t == ParamKeyType.BOOL:
self.put(key, bool(val))
else:
self.put(key, str(float(val)))
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
+43 -431
View File
@@ -8,7 +8,6 @@
inline static std::unordered_map<std::string, ParamKeyAttributes> keys = { inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}}, {"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
{"AdbEnabled", {PERSISTENT, BOOL}}, {"AdbEnabled", {PERSISTENT, BOOL}},
{"AlwaysAllowUploads", {PERSISTENT, BOOL, "0"}},
{"AlwaysOnDM", {PERSISTENT, BOOL}}, {"AlwaysOnDM", {PERSISTENT, BOOL}},
{"ApiCache_Device", {PERSISTENT, STRING}}, {"ApiCache_Device", {PERSISTENT, STRING}},
{"ApiCache_FirehoseStats", {PERSISTENT, JSON}}, {"ApiCache_FirehoseStats", {PERSISTENT, JSON}},
@@ -29,7 +28,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"CurrentBootlog", {PERSISTENT, STRING}}, {"CurrentBootlog", {PERSISTENT, STRING}},
{"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"DisableLogging", {PERSISTENT, BOOL, "0"}},
{"DisablePowerDown", {PERSISTENT, BOOL}}, {"DisablePowerDown", {PERSISTENT, BOOL}},
{"DisableUpdates", {PERSISTENT, BOOL}}, {"DisableUpdates", {PERSISTENT, BOOL}},
{"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}}, {"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}},
@@ -38,12 +37,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}}, {"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}},
{"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}}, {"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}},
{"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
{"AlphaLongitudinalEnabled", {PERSISTENT, BOOL}}, {"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY, BOOL}},
{"ExperimentalLongitudinalEnabled", {PERSISTENT, BOOL}},
{"ExperimentalMode", {PERSISTENT, BOOL}}, {"ExperimentalMode", {PERSISTENT, BOOL}},
{"ExperimentalModeConfirmed", {PERSISTENT, BOOL}}, {"ExperimentalModeConfirmed", {PERSISTENT, BOOL}},
{"PersistExperimentalState", {PERSISTENT, BOOL, "0", "0", 1}},
{"PersistedCEStatus", {PERSISTENT, INT, "0", "0"}},
{"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"ForcePowerDown", {PERSISTENT, BOOL}}, {"ForcePowerDown", {PERSISTENT, BOOL}},
{"GitBranch", {PERSISTENT, STRING}}, {"GitBranch", {PERSISTENT, STRING}},
@@ -70,7 +66,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LanguageSetting", {PERSISTENT, STRING, "main_en"}}, {"LanguageSetting", {PERSISTENT, STRING, "en"}},
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}}, {"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"LastGPSPosition", {PERSISTENT, STRING}}, {"LastGPSPosition", {PERSISTENT, STRING}},
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
@@ -85,7 +81,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LiveParameters", {PERSISTENT, JSON}}, {"LiveParameters", {PERSISTENT, JSON}},
{"LiveParametersV2", {PERSISTENT, BYTES}}, {"LiveParametersV2", {PERSISTENT, BYTES}},
{"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}}, {"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}},
{"LateralManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LocationFilterInitialState", {PERSISTENT, BYTES}}, {"LocationFilterInitialState", {PERSISTENT, BYTES}},
{"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LongitudinalPersonality", {PERSISTENT, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}}, {"LongitudinalPersonality", {PERSISTENT, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}},
@@ -116,13 +111,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
{"SecOCKey", {PERSISTENT | DONT_LOG, STRING}}, {"SecOCKey", {PERSISTENT | DONT_LOG, STRING}},
{"ShowDebugInfo", {PERSISTENT, BOOL}}, {"ShowDebugInfo", {PERSISTENT, BOOL}},
{"ShowAllToggles", {PERSISTENT, BOOL, "0", "0", 3}},
{"UsePrebuilt", {PERSISTENT, BOOL, "1"}},
{"RouteCount", {PERSISTENT, INT, "0"}}, {"RouteCount", {PERSISTENT, INT, "0"}},
{"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"SshEnabled", {PERSISTENT, BOOL}}, {"SshEnabled", {PERSISTENT, BOOL}},
{"TermsVersion", {PERSISTENT, STRING}},
{"TrainingVersion", {PERSISTENT, STRING}},
{"UbloxAvailable", {PERSISTENT, BOOL}}, {"UbloxAvailable", {PERSISTENT, BOOL}},
{"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"UpdateFailedCount", {CLEAR_ON_MANAGER_START, INT}}, {"UpdateFailedCount", {CLEAR_ON_MANAGER_START, INT}},
@@ -138,423 +129,44 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"Version", {PERSISTENT, STRING}}, {"Version", {PERSISTENT, STRING}},
{"dp_dev_last_log", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
// StarPilot variables {"dp_dev_reset_conf", {CLEAR_ON_MANAGER_START, BOOL, "0"}},
{"AccelerationPath", {PERSISTENT, BOOL, "1", "0", 2}}, {"dp_dev_beep", {PERSISTENT, BOOL, "0"}},
{"AccelerationProfile", {PERSISTENT, INT, "2", "0", 0}}, {"dp_dev_is_rhd", {PERSISTENT, BOOL, "0"}},
{"AdjacentLeadsUI", {PERSISTENT, BOOL, "1", "0", 3}}, {"dp_lat_alka", {PERSISTENT, BOOL, "0"}},
{"AdjacentPath", {PERSISTENT, BOOL, "0", "0", 3}}, {"dp_ui_display_mode", {PERSISTENT, INT, "0"}},
{"AdjacentPathMetrics", {PERSISTENT, BOOL, "0", "0", 3}}, {"dp_dev_model_selected", {PERSISTENT, STRING}},
{"AdvancedCustomUI", {PERSISTENT, BOOL, "0", "0", 2}}, {"dp_dev_model_list", {PERSISTENT, STRING}},
{"AdvancedLateralTune", {PERSISTENT, BOOL, "1", "0", 2}}, {"dp_lat_lca_speed", {PERSISTENT, INT, "20"}},
{"AdvancedLongitudinalTune", {PERSISTENT, BOOL, "1", "0", 3}}, {"dp_lat_lca_auto_sec", {PERSISTENT, FLOAT, "0.0"}},
{"AggressiveFollow", {PERSISTENT, FLOAT, "1.25", "1.25", 2}}, {"dp_dev_go_off_road", {CLEAR_ON_MANAGER_START, BOOL}},
{"AggressiveFollowHigh", {PERSISTENT, FLOAT, "1.25", "1.25", 2}}, {"dp_ui_hide_hud_speed_kph", {PERSISTENT, INT, "0"}},
{"AggressiveJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}}, {"dp_lon_ext_radar", {PERSISTENT, BOOL, "0"}},
{"AggressiveJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}}, {"dp_lat_road_edge_detection", {PERSISTENT, BOOL, "0"}},
{"AggressiveJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}}, {"dp_ui_rainbow", {PERSISTENT, BOOL, "0"}},
{"AggressiveJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}}, {"dp_lon_acm", {PERSISTENT, BOOL, "0"}},
{"AggressiveJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}}, {"dp_lon_aem", {PERSISTENT, BOOL, "0"}},
{"AlertVolumeControl", {PERSISTENT, BOOL, "0", "0", 2}}, {"dp_lon_dtsc", {PERSISTENT, BOOL, "0"}},
{"AlwaysOnLateral", {PERSISTENT, BOOL, "1", "0", 0}}, {"dp_lon_apm", {PERSISTENT, BOOL, "0"}},
{"AlwaysOnLateralLKAS", {PERSISTENT, BOOL, "1", "0", 2}}, {"dp_lon_dasr", {PERSISTENT, BOOL, "0"}},
{"ApiCache_DriveStats", {PERSISTENT, JSON, "{}", "{}"}}, {"dp_dev_audible_alert_mode", {PERSISTENT, INT, "0"}},
{"AutomaticallyDownloadModels", {PERSISTENT, BOOL, "1", "0", 1}}, {"dp_dev_auto_shutdown_in", {PERSISTENT, INT, "-5"}},
{"AutomaticUpdates", {PERSISTENT, BOOL, "1", "1", 0}}, {"dp_ui_lead", {PERSISTENT, INT, "0"}},
{"AvailableModelNames", {PERSISTENT, STRING, "", "", 1}}, {"dp_dev_opview", {PERSISTENT, BOOL, "0"}},
{"AvailableModelSeries", {PERSISTENT, STRING, "", "", 1}}, {"dp_dev_dashy", {PERSISTENT, BOOL, "0"}},
{"AvailableModels", {PERSISTENT, STRING, "", "", 1}}, {"dp_maa_route", {CLEAR_ON_MANAGER_START, JSON}},
{"BlacklistedModels", {PERSISTENT, STRING, "", "", 2}}, {"dp_maa_destination", {PERSISTENT, JSON}},
{"BootLogo", {PERSISTENT, STRING, "starpilot", "stock", 0}}, {"dp_maa_places", {PERSISTENT, JSON}},
{"BuildMetadata", {PERSISTENT, STRING, "", "", 0}}, {"dp_dev_delay_loggerd", {PERSISTENT, INT, "0"}},
{"BlindSpotMetrics", {PERSISTENT, BOOL, "1", "0", 3}}, {"dp_dev_disable_connect", {PERSISTENT, BOOL, "0"}},
{"BlindSpotPath", {PERSISTENT, BOOL, "1", "0", 1}}, {"dp_dev_tethering", {PERSISTENT, BOOL, "0"}},
{"BelowSteerSpeedVolume", {PERSISTENT, INT, "101", "101", 2}}, {"dp_ui_mici", {PERSISTENT, BOOL, "0"}},
{"BorderMetrics", {PERSISTENT, BOOL, "0", "0", 3}}, {"dp_lat_offset_cm", {PERSISTENT, INT, "0"}},
{"CalibratedLateralAcceleration", {PERSISTENT, FLOAT, "2.0", "2.0", 2}}, {"dp_toyota_door_auto_lock_unlock", {PERSISTENT, BOOL, "0"}},
{"CalibrationProgress", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"dp_toyota_tss1_sng", {PERSISTENT, BOOL, "0"}},
{"CameraView", {PERSISTENT, INT, "3", "0", 2}}, {"dp_toyota_stock_lon", {PERSISTENT, BOOL, "0"}},
{"CancelDownloadMaps", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}}, {"dp_vag_a0_sng", {PERSISTENT, BOOL, "0"}},
{"CancelModelDownload", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}}, {"dp_vag_pq_steering_patch", {PERSISTENT, BOOL, "0"}},
{"CancelThemeDownload", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}}, {"dp_vag_avoid_eps_lockout", {PERSISTENT, BOOL, "0"}},
{"CarMake", {PERSISTENT, STRING, "mock", "mock", 0}}, {"dp_honda_nidec_stock_long", {PERSISTENT, BOOL, "0"}},
{"CarModel", {PERSISTENT, STRING, "MOCK", "MOCK", 0}},
{"CarModelName", {PERSISTENT, STRING, "", "", 0}},
{"CECurves", {PERSISTENT, BOOL, "0", "0", 1}},
{"CECurvesLead", {PERSISTENT, BOOL, "0", "0", 1}},
{"CELead", {PERSISTENT, BOOL, "1", "0", 1}},
{"CEModelStopTime", {PERSISTENT, FLOAT, "7.0", "0.0", 2}},
{"CESignalLaneDetection", {PERSISTENT, BOOL, "1", "0", 2}},
{"CESignalSpeed", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"CESlowerLead", {PERSISTENT, BOOL, "1", "0", 1}},
{"CESpeed", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"CESpeedLead", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"CEStatus", {CLEAR_ON_OFFROAD_TRANSITION, INT, "0", "0"}},
{"CEStopLights", {PERSISTENT, BOOL, "1", "0", 1}},
{"CEStoppedLead", {PERSISTENT, BOOL, "1", "0", 1}},
{"ClusterOffset", {PERSISTENT, FLOAT, "1.015", "1.015", 2}},
{"ColorScheme", {PERSISTENT, STRING, "frog", "stock", 0}},
{"ColorToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"BootLogoToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"Compass", {PERSISTENT, BOOL, "0", "0", 1}},
{"CommunityFavorites", {PERSISTENT, STRING, "", "", 1}},
{"ConditionalExperimental", {PERSISTENT, BOOL, "1", "0", 1}},
{"CurvatureData", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"CurveSpeedController", {PERSISTENT, BOOL, "1", "0", 1}},
{"CustomAlerts", {PERSISTENT, BOOL, "0", "0", 0}},
{"CustomAccelProfile", {PERSISTENT, BOOL, "0", "0", 3}},
{"CustomAccelProfileInitialized", {PERSISTENT, BOOL, "0", "0", 3}},
{"CustomAccelProfile0MPH", {PERSISTENT, FLOAT, "3.0", "3.0", 3}},
{"CustomAccelProfile11MPH", {PERSISTENT, FLOAT, "2.5", "2.5", 3}},
{"CustomAccelProfile22MPH", {PERSISTENT, FLOAT, "2.0", "2.0", 3}},
{"CustomAccelProfile34MPH", {PERSISTENT, FLOAT, "1.5", "1.5", 3}},
{"CustomAccelProfile45MPH", {PERSISTENT, FLOAT, "1.0", "1.0", 3}},
{"CustomAccelProfile56MPH", {PERSISTENT, FLOAT, "0.8", "0.8", 3}},
{"CustomAccelProfile89MPH", {PERSISTENT, FLOAT, "0.6", "0.6", 3}},
{"CustomCruise", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
{"CustomCruiseLong", {PERSISTENT, FLOAT, "5.0", "5.0", 2}},
{"CustomPersonalities", {PERSISTENT, BOOL, "0", "0", 2}},
{"TrafficPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"AggressivePersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"StandardPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"RelaxedPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"CustomThemes", {PERSISTENT, BOOL, "1", "0", 0}},
{"CustomUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"DebugMode", {CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0", "0"}},
{"DecelerationProfile", {PERSISTENT, INT, "1", "0", 2}},
{"DeveloperMetrics", {PERSISTENT, BOOL, "1", "0", 3}},
{"DeveloperSidebar", {PERSISTENT, BOOL, "1", "0", 3}},
{"DeveloperSidebarMetric1", {PERSISTENT, INT, "1", "0", 3}},
{"DeveloperSidebarMetric2", {PERSISTENT, INT, "2", "0", 3}},
{"DeveloperSidebarMetric3", {PERSISTENT, INT, "3", "0", 3}},
{"DeveloperSidebarMetric4", {PERSISTENT, INT, "4", "0", 3}},
{"DeveloperSidebarMetric5", {PERSISTENT, INT, "5", "0", 3}},
{"DeveloperSidebarMetric6", {PERSISTENT, INT, "6", "0", 3}},
{"DeveloperSidebarMetric7", {PERSISTENT, INT, "7", "0", 3}},
{"DeveloperUI", {PERSISTENT, BOOL, "0", "0", 3}},
{"DeveloperWidgets", {PERSISTENT, BOOL, "1", "0", 3}},
{"DeviceManagement", {PERSISTENT, BOOL, "1", "0", 1}},
{"DeviceShutdown", {PERSISTENT, INT, "9", "33", 1}},
{"DisableOnroadUploads", {PERSISTENT, BOOL, "0", "0", 2}},
{"DisableOpenpilotLongitudinal", {PERSISTENT, BOOL, "0", "0", 0}},
{"DiscordUsername", {PERSISTENT, STRING, "", "", 0}},
{"DisengageVolume", {PERSISTENT, INT, "101", "101", 2}},
{"DistanceButtonControl", {PERSISTENT, INT, "1", "0", 2}},
{"DistanceIconPack", {PERSISTENT, STRING, "stock", "stock", 0}},
{"DistanceIconToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"DownloadableBootLogos", {PERSISTENT, STRING, "", ""}},
{"DownloadableColors", {PERSISTENT, STRING, "", ""}},
{"DownloadableDistanceIcons", {PERSISTENT, STRING, "", ""}},
{"DownloadableIcons", {PERSISTENT, STRING, "", ""}},
{"DownloadableSignals", {PERSISTENT, STRING, "", ""}},
{"DownloadableSounds", {PERSISTENT, STRING, "", ""}},
{"DownloadableWheels", {PERSISTENT, STRING, "", ""}},
{"DownloadAllModels", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"DownloadMaps", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"DriverCamera", {PERSISTENT, BOOL, "0", "0", 1}},
{"Model", {PERSISTENT, STRING, "sc2", "sc2", 1}},
{"ModelVersion", {PERSISTENT, STRING, "v11", "v11", 1}},
{"DrivingModel", {PERSISTENT, STRING, "sc2", "sc2", 1}},
{"DrivingModelName", {PERSISTENT, STRING, "South Carolina", "South Carolina", 1}},
{"DrivingModelVersion", {PERSISTENT, STRING, "v11", "v11", 1}},
{"DynamicPathWidth", {PERSISTENT, BOOL, "0", "0", 2}},
{"DynamicPedalsOnUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"EngageVolume", {PERSISTENT, INT, "101", "101", 2}},
{"EVTuning", {PERSISTENT, BOOL, "0", "0", 3}},
{"Fahrenheit", {PERSISTENT, BOOL, "0", "0", 3}},
{"FlashPanda", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"GMPedalLongitudinal", {PERSISTENT, BOOL, "1", "1", 2}},
{"LongPitch", {PERSISTENT, BOOL, "1", "0", 2}},
{"RemoteStartBootsComma", {PERSISTENT, BOOL, "0", "0"}},
{"RemapCancelToDistance", {PERSISTENT, BOOL, "0", "0"}},
{"ForceAutoTune", {PERSISTENT, BOOL, "0", "0", 3}},
{"ForceAutoTuneOff", {PERSISTENT, BOOL, "1", "0", 2}},
{"ForceFingerprint", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceOffroad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"ForceOnroad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"ForceStops", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceStandstill", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
{"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}},
{"StarPilotApiToken", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"StarPilotCarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES, "", ""}},
{"StarPilotCarParamsPersistent", {PERSISTENT, BYTES, "", ""}},
{"StarPilotDongleId", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"StarPilotStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"StarPilotTogglesUpdated", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"FrogsGoMoosTweak", {PERSISTENT, BOOL, "1", "0", 2}},
{"GoatScream", {PERSISTENT, BOOL, "0", "0", 1}},
{"GoatScreamCriticalAlerts", {PERSISTENT, BOOL, "0", "0", 1}},
{"GreenLightAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"HideAlerts", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideLeadMarker", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideMaxSpeed", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideSpeed", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideSpeedLimit", {PERSISTENT, BOOL, "0", "0", 2}},
{"HigherBitrate", {PERSISTENT, BOOL, "0", "0", 2}},
{"HolidayThemes", {PERSISTENT, BOOL, "1", "0", 0}},
{"HumanAcceleration", {PERSISTENT, BOOL, "0", "0", 2}},
{"HumanFollowing", {PERSISTENT, BOOL, "0", "0", 2}},
{"HumanLaneChanges", {PERSISTENT, BOOL, "0", "0", 2}},
{"IconPack", {PERSISTENT, STRING, "frog-animated", "stock", 0}},
{"IconToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"IncreasedStoppedDistance", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"IncreasedStoppedDistanceLowVisibility", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreasedStoppedDistanceRain", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreasedStoppedDistanceRainStorm", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreasedStoppedDistanceSnow", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingLowVisibility", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingRain", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingRainStorm", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingSnow", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseThermalLimits", {PERSISTENT, BOOL, "0", "0", 2}},
{"IssueReported", {CLEAR_ON_MANAGER_START, JSON, "{}", "{}"}},
{"KonikDongleId", {PERSISTENT, STRING, "", "", 0}},
{"KonikMinutes", {PERSISTENT, INT, "0", "0", 0}},
{"LaneChanges", {PERSISTENT, BOOL, "1", "1", 0}},
{"LaneChangeTime", {PERSISTENT, FLOAT, "1.0", "0.0", 1}},
{"LaneDetectionWidth", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"LaneLinesWidth", {PERSISTENT, FLOAT, "4.0", "2.0", 2}},
{"LastMapsUpdate", {PERSISTENT, STRING, "", ""}},
{"LateralTune", {PERSISTENT, BOOL, "1", "0", 1}},
{"LeadDepartingAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"LeadDetectionThreshold", {PERSISTENT, INT, "35", "50", 3}},
{"LeadInfo", {PERSISTENT, BOOL, "1", "0", 3}},
{"LKASButtonControl", {PERSISTENT, INT, "5", "0", 2}},
{"LockDoors", {PERSISTENT, BOOL, "1", "0", 0}},
{"LockDoorsTimer", {PERSISTENT, INT, "0", "0", 0}},
{"LongDistanceButtonControl", {PERSISTENT, INT, "5", "0", 2}},
{"LongitudinalActuatorDelay", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"LongitudinalActuatorDelayStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"LateralManeuverStatus", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON, "{}", "{}"}},
{"LongitudinalManeuverPaddleMode", {PERSISTENT, STRING, "auto", "auto"}},
{"LongitudinalManeuverStatus", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON, "{}", "{}"}},
{"LongitudinalTune", {PERSISTENT, BOOL, "1", "0", 0}},
{"LoudBlindspotAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"LowVoltageShutdown", {PERSISTENT, FLOAT, "11.8", "11.8", 3}},
{"ManualUpdateInitiated", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"MapAcceleration", {PERSISTENT, BOOL, "0", "0", 1}},
{"MapboxPublicKey", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"MapBoxRequests", {PERSISTENT, JSON, "{}", "{}"}},
{"MapboxSecretKey", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"MapDeceleration", {PERSISTENT, BOOL, "0", "0", 1}},
{"MapdSettings", {PERSISTENT, JSON, "{}", "{}"}},
{"MapGears", {PERSISTENT, BOOL, "0", "0", 2}},
{"MapsSelected", {PERSISTENT, STRING, "", "", 0}},
{"MapSpeedLimit", {CLEAR_ON_MANAGER_START, FLOAT, "0.0", "0.0"}},
{"NextMapSpeedLimit", {CLEAR_ON_MANAGER_START, JSON, "{}", "{}"}},
{"VisionSpeedLimit", {CLEAR_ON_MANAGER_START, FLOAT, "0.0", "0.0"}},
{"VisionSpeedLimitConfidence", {CLEAR_ON_MANAGER_START, FLOAT, "0.0", "0.0"}},
{"VisionSpeedLimitBookmarkCount", {CLEAR_ON_MANAGER_START, INT, "0", "0"}},
{"VisionSpeedLimitDebugSession", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"VisionSpeedLimitLastEvent", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"VisionSpeedLimitStatus", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"VisionSpeedLimitStream", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"MaxDesiredAcceleration", {PERSISTENT, FLOAT, "4.0", "2.0", 2}},
{"MinimumBackupSize", {PERSISTENT, INT, "0", "0"}},
{"MinimumLaneChangeSpeed", {PERSISTENT, FLOAT, "20.0", "20.0", 2}},
{"ModelDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ModelDrivesAndScores", {PERSISTENT, JSON, "{}", "{}"}},
{"ModelReleasedDates", {PERSISTENT, STRING, "", "", 1}},
{"ModelRandomizer", {PERSISTENT, BOOL, "0", "0", 2}},
{"ModelSortMode", {PERSISTENT, STRING, "alphabetical", "alphabetical", 1}},
{"ModelToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ModelUI", {PERSISTENT, BOOL, "1", "0", 2}},
{"ModelVersions", {PERSISTENT, STRING, "", "", 1}},
{"NavigationUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"NNFF", {PERSISTENT, BOOL, "0", "0", 2}},
{"NNFFLite", {PERSISTENT, BOOL, "0", "0", 2}},
{"NNFFModelName", {CLEAR_ON_MANAGER_START, STRING, "", "", 0}},
{"NoLogging", {PERSISTENT, BOOL, "0", "0", 2}},
{"NoUploads", {PERSISTENT, BOOL, "0", "0", 2}},
{"NudgelessLaneChange", {PERSISTENT, BOOL, "1", "0", 0}},
{"NumericalTemp", {PERSISTENT, BOOL, "1", "0", 3}},
{"Offset1", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset2", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset3", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset4", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset5", {PERSISTENT, FLOAT, "10.0", "0.0", 0}},
{"Offset6", {PERSISTENT, FLOAT, "10.0", "0.0", 0}},
{"Offset7", {PERSISTENT, FLOAT, "10.0", "0.0", 0}},
{"OneLaneChange", {PERSISTENT, BOOL, "1", "0", 2}},
{"OnroadDistanceButton", {PERSISTENT, BOOL, "0", "0", 0}},
{"OnroadDistanceButtonPressed", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"openpilotMinutes", {PERSISTENT, INT, "0", "0", 0}},
{"OverpassRequests", {PERSISTENT, JSON, "{}", "{}"}},
{"PathEdgeWidth", {PERSISTENT, FLOAT, "20.0", "0.0", 2}},
{"PathWidth", {PERSISTENT, FLOAT, "6.1", "5.9", 2}},
{"PauseAOLOnBrake", {PERSISTENT, BOOL, "0", "0", 1}},
{"PauseLateralOnSignal", {PERSISTENT, BOOL, "0", "0", 1}},
{"PauseLateralSpeed", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"PedalsOnUI", {PERSISTENT, BOOL, "0", "0", 1}},
{"PondPaired", {PERSISTENT, BOOL, "0", "0", 0}},
{"PondUploadPending", {PERSISTENT, BOOL, "0", "0", 0}},
{"PreferredSchedule", {PERSISTENT, INT, "2", "0", 0}},
{"PreviousSpeedLimit", {PERSISTENT, FLOAT, "0.0", "0.0"}},
{"PromptDistractedVolume", {PERSISTENT, INT, "101", "101", 2}},
{"PromptVolume", {PERSISTENT, INT, "101", "101", 2}},
{"QOLLateral", {PERSISTENT, BOOL, "1", "0", 1}},
{"QOLLongitudinal", {PERSISTENT, BOOL, "1", "0", 1}},
{"QOLVisuals", {PERSISTENT, BOOL, "1", "0", 0}},
{"RadarTracksUI", {PERSISTENT, BOOL, "0", "0", 3}},
{"RainbowPath", {PERSISTENT, BOOL, "0", "0", 1}},
{"RandomEvents", {PERSISTENT, BOOL, "0", "0", 1}},
{"RandomThemes", {PERSISTENT, BOOL, "0", "0", 1}},
{"RandomThemesHolidays", {PERSISTENT, BOOL, "0", "0", 1}},
{"ReduceAccelerationLowVisibility", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceAccelerationRain", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceAccelerationRainStorm", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceAccelerationSnow", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationLowVisibility", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationRain", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationRainStorm", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationSnow", {PERSISTENT, INT, "0", "0", 2}},
{"RefuseVolume", {PERSISTENT, INT, "101", "101", 2}},
{"RelaxedFollow", {PERSISTENT, FLOAT, "1.75", "1.75", 2}},
{"RelaxedFollowHigh", {PERSISTENT, FLOAT, "1.75", "1.75", 2}},
{"RelaxedJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"RelaxedJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"RelaxedJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"RelaxedJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"RelaxedJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"ReverseCruise", {PERSISTENT, BOOL, "0", "0", 1}},
{"RecoveryPower", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
{"RoadEdgesWidth", {PERSISTENT, FLOAT, "2.0", "2.0", 2}},
{"RoadNameUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"RotatingWheel", {PERSISTENT, BOOL, "1", "0", 1}},
{"ScreenBrightness", {PERSISTENT, INT, "101", "101", 2}},
{"ScreenBrightnessOnroad", {PERSISTENT, INT, "101", "101", 2}},
{"ScreenManagement", {PERSISTENT, BOOL, "1", "0", 1}},
{"ScreenRecorder", {PERSISTENT, BOOL, "1", "0", 2}},
{"ScreenTimeout", {PERSISTENT, INT, "30", "30", 2}},
{"ScreenTimeoutOnroad", {PERSISTENT, INT, "30", "10", 2}},
{"SecOCKeys", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"SafeMode", {PERSISTENT, BOOL, "0", "0", 0}},
{"SafeModeBackup", {PERSISTENT, JSON, "{}", "{}"}},
{"SetSpeedLimit", {PERSISTENT, BOOL, "0", "0", 1}},
{"SetSpeedOffset", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"ShowCEMStatus", {PERSISTENT, BOOL, "1", "0", 2}},
{"ShowCPU", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShowCSCStatus", {PERSISTENT, BOOL, "1", "0", 2}},
{"ShowGPU", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowIP", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowMemoryUsage", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShownToggleDescriptions", {PERSISTENT, JSON, "{}", "{}"}},
{"ShowSLCOffset", {PERSISTENT, BOOL, "1", "0", 0}},
{"ShowSpeedLimits", {PERSISTENT, BOOL, "1", "0", 1}},
{"ShowSteering", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowStoppingPoint", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShowStoppingPointMetrics", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShowStorageLeft", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowStorageUsed", {PERSISTENT, BOOL, "0", "0", 3}},
{"SidebarMetrics", {PERSISTENT, BOOL, "1", "0", 3}},
{"SidebarOpen", {PERSISTENT, BOOL, "0", "0", 0}},
{"SignalAnimation", {PERSISTENT, STRING, "frog", "stock", 0}},
{"SignalMetrics", {PERSISTENT, BOOL, "0", "0", 3}},
{"SignalToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"SimpleMode", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCConfirmation", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCConfirmationHigher", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCConfirmationLower", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCFallback", {PERSISTENT, INT, "2", "0", 1}},
{"SLCLookaheadHigher", {PERSISTENT, INT, "0", "0", 2}},
{"SLCLookaheadLower", {PERSISTENT, INT, "0", "0", 2}},
{"SLCMapboxFiller", {PERSISTENT, BOOL, "1", "0", 1}},
{"SLCOverride", {PERSISTENT, INT, "1", "0", 1}},
{"SLCPriority", {PERSISTENT, STRING, "", "", 2}},
{"SLCPriority1", {PERSISTENT, STRING, "Map Data", "Map Data", 2}},
{"SLCPriority2", {PERSISTENT, STRING, "Dashboard", "Dashboard", 2}},
{"SNGHack", {PERSISTENT, BOOL, "1", "0", 2}},
{"SoundPack", {PERSISTENT, STRING, "frog", "stock", 0}},
{"SoundToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"SpeedLimitAccepted", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"SpeedLimitChangedAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"SpeedLimitController", {PERSISTENT, BOOL, "0", "0", 0}},
{"SpeedLimitFiller", {PERSISTENT, BOOL, "0", "0", 0}},
{"SpeedLimits", {PERSISTENT | DONT_LOG, JSON, "[]", "[]"}},
{"SpeedLimitsFiltered", {PERSISTENT | DONT_LOG, JSON, "[]", "[]"}},
{"SpeedLimitSources", {PERSISTENT, BOOL, "0", "0", 3}},
{"VisionSpeedLimitAutoBookmark", {PERSISTENT, BOOL, "0", "0", 0}},
{"VisionSpeedLimitAutoPreserveSegment", {PERSISTENT, BOOL, "0", "0", 0}},
{"VisionSpeedLimitDetection", {PERSISTENT, BOOL, "0", "0", 0}},
{"VisionSpeedLimitTrainingCollector", {PERSISTENT, BOOL, "1", "1", 0}},
{"StandardFollow", {PERSISTENT, FLOAT, "1.45", "1.45", 2}},
{"StandardFollowHigh", {PERSISTENT, FLOAT, "1.45", "1.45", 2}},
{"StandardJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandardJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"StandardJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandardJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandardJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandbyMode", {PERSISTENT, BOOL, "0", "0", 1}},
{"StartAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StartAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StartupMessageBottom", {PERSISTENT, STRING, "Human-tested, frog-approved 🐸", "Always keep hands on wheel and eyes on road", 0}},
{"StartupMessageTop", {PERSISTENT, STRING, "Hop in and buckle up!", "Be ready to take over at any time", 0}},
{"StaticPedalsOnUI", {PERSISTENT, BOOL, "0", "0", 1}},
{"SteerDelay", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerDelayStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerFriction", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerFrictionStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerKP", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerKPStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerLatAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerLatAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerOffset", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerOffsetStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerRatio", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerRatioStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StockDongleId", {PERSISTENT, STRING, "", ""}},
{"StopAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StopAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StoppedTimer", {PERSISTENT, BOOL, "0", "0", 1}},
{"StopDistance", {PERSISTENT, FLOAT, "6.0", "6.0", 2}},
{"StoppingDecelRate", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StoppingDecelRateStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SwitchbackModeCooldown", {PERSISTENT, INT, "5", "0", 2}},
{"SwitchbackModeEnabled", {CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0", "0"}},
{"SubaruSNG", {PERSISTENT, BOOL, "1", "0", 2}},
{"TacoTune", {PERSISTENT, BOOL, "0", "0", 2}},
{"TacoTuneHacks", {PERSISTENT, BOOL, "0", "0", 2}},
{"TestAlert", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"TetheringEnabled", {PERSISTENT, INT, "0", "0", 0}},
{"ThemeDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ThemesDownloaded", {PERSISTENT, JSON, "{}", "{}"}},
{"Timezone", {PERSISTENT, STRING, "", ""}},
{"TinygradUpdateAvailable", {PERSISTENT, BOOL, "0", "0", 1}},
{"ToyotaDoors", {PERSISTENT, BOOL, "1", "0", 0}},
{"TrafficFollow", {PERSISTENT, FLOAT, "0.5", "0.5", 2}},
{"TrafficJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TrafficJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"TrafficJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TrafficJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TrafficJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TruckTuning", {PERSISTENT, BOOL, "0", "0", 3}},
{"TuningLevel", {PERSISTENT, INT, "0", "0", 0}},
{"TuningLevelConfirmed", {PERSISTENT, BOOL, "0", "0", 0}},
{"TurnDesires", {PERSISTENT, BOOL, "0", "0", 2}},
{"UnlockDoors", {PERSISTENT, BOOL, "1", "0", 0}},
{"Updated", {PERSISTENT, STRING, "0", "0"}},
{"UpdateSpeedLimits", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UpdateSpeedLimitsStatus", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"UpdateTinygrad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UpdateWheelImage", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UseActiveTheme", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UseKonikServer", {PERSISTENT, BOOL, "0", "0", 2}},
{"UseSI", {PERSISTENT, BOOL, "1", "1", 3}},
{"UserFavorites", {PERSISTENT, STRING, "", "", 1}},
{"UseVienna", {PERSISTENT, BOOL, "0", "0", 1}},
{"VEgoStarting", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VEgoStartingStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VEgoStopping", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VEgoStoppingStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VeryLongDistanceButtonControl", {PERSISTENT, INT, "6", "0", 2}},
{"VoltSNG", {PERSISTENT, BOOL, "0", "0", 2}},
{"WarningImmediateVolume", {PERSISTENT, INT, "101", "101", 2}},
{"WarningSoftVolume", {PERSISTENT, INT, "101", "101", 2}},
{"WeatherPresets", {PERSISTENT, BOOL, "0", "0", 2}},
{"WeatherToken", {PERSISTENT | DONT_LOG, STRING, "", "", 2}},
{"WheelControls", {PERSISTENT, STRING, "", "", 2}},
{"WheelIcon", {PERSISTENT, STRING, "frog", "stock", 0}},
{"WheelSpeed", {PERSISTENT, BOOL, "0", "0", 2}},
{"WheelToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
}; };
-19413
View File
File diff suppressed because it is too large Load Diff
+7 -68
View File
@@ -20,9 +20,6 @@ cdef extern from "common/params.h":
CLEAR_ON_IGNITION_ON CLEAR_ON_IGNITION_ON
ALL ALL
# StarPilot variables
DONT_LOG
cpdef enum ParamKeyType: cpdef enum ParamKeyType:
STRING STRING
BOOL BOOL
@@ -33,7 +30,7 @@ cdef extern from "common/params.h":
BYTES BYTES
cdef cppclass c_Params "Params": cdef cppclass c_Params "Params":
c_Params(string, bool) except + nogil c_Params(string) except + nogil
string get(string, bool) nogil string get(string, bool) nogil
bool getBool(string, bool) nogil bool getBool(string, bool) nogil
int remove(string) nogil int remove(string) nogil
@@ -48,13 +45,6 @@ cdef extern from "common/params.h":
void clearAll(ParamKeyFlag) void clearAll(ParamKeyFlag)
vector[string] allKeys() vector[string] allKeys()
# StarPilot variables
ParamKeyFlag getKeyFlag(string) nogil
optional[string] getStockValue(string) nogil
int getTuningLevel(string) nogil
PYTHON_2_CPP = { PYTHON_2_CPP = {
(str, STRING): lambda v: v, (str, STRING): lambda v: v,
(builtins.bool, BOOL): lambda v: "1" if v else "0", (builtins.bool, BOOL): lambda v: "1" if v else "0",
@@ -65,35 +55,12 @@ PYTHON_2_CPP = {
(list, JSON): json.dumps, (list, JSON): json.dumps,
(bytes, BYTES): lambda v: v, (bytes, BYTES): lambda v: v,
} }
def _decode_int(v):
decoded = v.decode("utf-8")
try:
return int(decoded)
except ValueError:
return int(float(decoded))
def _decode_time(v):
decoded = v.decode("utf-8")
try:
return datetime.datetime.fromisoformat(decoded)
except ValueError:
for fmt in ("%B %d, %Y - %I:%M%p", "%B %d, %Y - %I:%M %p"):
try:
return datetime.datetime.strptime(decoded, fmt)
except ValueError:
pass
raise
CPP_2_PYTHON = { CPP_2_PYTHON = {
STRING: lambda v: v.decode("utf-8"), STRING: lambda v: v.decode("utf-8"),
BOOL: lambda v: v == b"1", BOOL: lambda v: v == b"1",
INT: _decode_int, INT: int,
FLOAT: float, FLOAT: float,
TIME: _decode_time, TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")),
JSON: json.loads, JSON: json.loads,
BYTES: lambda v: v, BYTES: lambda v: v,
} }
@@ -108,27 +75,14 @@ cdef class Params:
cdef c_Params* p cdef c_Params* p
cdef str d cdef str d
# StarPilot variables def __cinit__(self, d=""):
cdef bool m
cdef bool return_defaults
def __cinit__(self, d="", *, memory=False, return_defaults=False):
cdef string path = <string>d.encode() cdef string path = <string>d.encode()
# StarPilot variables
cdef bool c_memory = memory
with nogil: with nogil:
self.p = new c_Params(path, c_memory) self.p = new c_Params(path)
self.d = d self.d = d
# StarPilot variables
self.m = memory
self.return_defaults = return_defaults or memory
def __reduce__(self): def __reduce__(self):
return (type(self), (self.d, self.m, self.return_defaults)) return (type(self), (self.d,))
def __dealloc__(self): def __dealloc__(self):
del self.p del self.p
@@ -165,7 +119,7 @@ cdef class Params:
with nogil: with nogil:
val = self.p.get(k, block) val = self.p.get(k, block)
default_val = (default.value() if default.has_value() else None) if (return_default or self.return_defaults and not block) else None default_val = (default.value() if default.has_value() else None) if return_default else None
if val == b"": if val == b"":
if block: if block:
# If we got no value while running in blocked mode # If we got no value while running in blocked mode
@@ -240,18 +194,3 @@ cdef class Params:
cdef string k = self.check_key(key) cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k) cdef ParamKeyType t = self.p.getKeyType(k)
return self._cpp2python(t, value, None, key) return self._cpp2python(t, value, None, key)
# StarPilot variables
def get_key_flag(self, key):
return self.p.getKeyFlag(self.check_key(key))
def get_stock_value(self, key):
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
cdef optional[string] stock = self.p.getStockValue(k)
return self._cpp2python(t, stock.value(), None, key) if stock.has_value() else None
def get_tuning_level(self, key):
cdef string k = self.check_key(key)
cdef optional[int] level = self.p.getTuningLevel(k)
return level.value() if level.has_value() else 0
Binary file not shown.
+1 -4
View File
@@ -30,10 +30,7 @@ class Priority:
def set_core_affinity(cores: list[int]) -> None: def set_core_affinity(cores: list[int]) -> None:
if sys.platform == 'linux' and not PC: if sys.platform == 'linux' and not PC:
try: os.sched_setaffinity(0, cores)
os.sched_setaffinity(0, cores)
except OSError:
pass
def config_realtime_process(cores: int | list[int], priority: int) -> None: def config_realtime_process(cores: int | list[int], priority: int) -> None:
+2 -20
View File
@@ -5,28 +5,10 @@ from openpilot.common.basedir import BASEDIR
class Spinner: class Spinner:
def __init__(self): def __init__(self):
self.spinner_proc = None
# Prefer the legacy compiled spinner on device.
legacy_spinner = os.path.join(BASEDIR, "selfdrive", "ui", "spinner")
if os.path.isfile("/TICI") and os.path.isfile(legacy_spinner) and os.access(legacy_spinner, os.X_OK):
try:
self.spinner_proc = subprocess.Popen([legacy_spinner],
stdin=subprocess.PIPE,
cwd=os.path.join(BASEDIR, "selfdrive", "ui"),
close_fds=True)
return
except OSError:
self.spinner_proc = None
# Raylib spinner requires Python deps from the repo virtualenv.
spinner_cwd = os.path.join(BASEDIR, "system", "ui")
venv_python = os.path.join(BASEDIR, ".venv", "bin", "python")
python_exec = venv_python if os.path.isfile(venv_python) else "python3"
try: try:
self.spinner_proc = subprocess.Popen([python_exec, "./spinner.py"], self.spinner_proc = subprocess.Popen(["./spinner.py"],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
cwd=spinner_cwd, cwd=os.path.join(BASEDIR, "system", "ui"),
close_fds=True) close_fds=True)
except OSError: except OSError:
self.spinner_proc = None self.spinner_proc = None
-10
View File
@@ -101,16 +101,6 @@ class TestParams:
assert q.get("CarParams") is None assert q.get("CarParams") is None
assert q.get("CarParams", True) == b"1" assert q.get("CarParams", True) == b"1"
def test_put_nonblocking_latest_value_wins(self, tmp_path):
q = Params(str(tmp_path))
for i in range(100):
q.put_nonblocking("CarParams", f"value-{i}".encode())
del q
r = Params(str(tmp_path))
assert r.get("CarParams") == b"value-99"
def test_params_all_keys(self): def test_params_all_keys(self):
keys = Params().all_keys() keys = Params().all_keys()
+2 -19
View File
@@ -7,27 +7,10 @@ from openpilot.common.basedir import BASEDIR
class TextWindow: class TextWindow:
def __init__(self, text): def __init__(self, text):
self.text_proc = None
# Prefer the legacy compiled text window on device.
legacy_text = os.path.join(BASEDIR, "selfdrive", "ui", "text")
if os.path.isfile("/TICI") and os.path.isfile(legacy_text) and os.access(legacy_text, os.X_OK):
try:
self.text_proc = subprocess.Popen([legacy_text, text],
stdin=subprocess.PIPE,
cwd=os.path.join(BASEDIR, "selfdrive", "ui"),
close_fds=True)
return
except OSError:
self.text_proc = None
text_cwd = os.path.join(BASEDIR, "system", "ui")
venv_python = os.path.join(BASEDIR, ".venv", "bin", "python")
python_exec = venv_python if os.path.isfile(venv_python) else "python3"
try: try:
self.text_proc = subprocess.Popen([python_exec, "./text.py", text], self.text_proc = subprocess.Popen(["./text.py", text],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
cwd=text_cwd, cwd=os.path.join(BASEDIR, "system", "ui"),
close_fds=True) close_fds=True)
except OSError: except OSError:
self.text_proc = None self.text_proc = None
Binary file not shown.
-5
View File
@@ -37,11 +37,6 @@ const double MS_TO_MPH = MS_TO_KPH * KM_TO_MILE;
const double METER_TO_MILE = KM_TO_MILE / 1000.0; const double METER_TO_MILE = KM_TO_MILE / 1000.0;
const double METER_TO_FOOT = 3.28084; const double METER_TO_FOOT = 3.28084;
// StarPilot variables
const double FOOT_TO_METER = 1. / METER_TO_FOOT;
const double CM_TO_INCH = 1. / 2.54;
const double INCH_TO_CM = 1. / CM_TO_INCH;
#define ALIGNED_SIZE(x, align) (((x) + (align)-1) & ~((align)-1)) #define ALIGNED_SIZE(x, align) (((x) + (align)-1) & ~((align)-1))
namespace util { namespace util {
-12
View File
@@ -1,12 +0,0 @@
#include <string>
#include "common/watchdog.h"
#include "common/util.h"
#include "system/hardware/hw.h"
const std::string watchdog_fn_prefix = Path::shm_path() + "/wd_"; // + <pid>
bool watchdog_kick(uint64_t ts) {
static std::string fn = watchdog_fn_prefix + std::to_string(getpid());
return util::write_file(fn.c_str(), &ts, sizeof(ts), O_WRONLY | O_CREAT) == 0;
}
-5
View File
@@ -1,5 +0,0 @@
#pragma once
#include <cstdint>
bool watchdog_kick(uint64_t ts);
-28
View File
@@ -1,28 +0,0 @@
import os
import time
import struct
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
WATCHDOG_FN = f"{Paths.shm_path()}/wd_"
_LAST_KICK = 0.0
_LAST_ERROR_LOG = 0.0
def kick_watchdog():
global _LAST_KICK, _LAST_ERROR_LOG
current_time = time.monotonic()
if current_time - _LAST_KICK < 1.0:
return True
try:
with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f:
f.write(struct.pack('<Q', int(current_time * 1e9)))
f.flush()
_LAST_KICK = current_time
return True
except OSError as e:
if current_time - _LAST_ERROR_LOG >= 5.0:
cloudlog.error(f"watchdog kick failed for pid {os.getpid()}: {e}")
_LAST_ERROR_LOG = current_time
return False
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" "$@"
-129
View File
@@ -1,129 +0,0 @@
# Laptop Device Build (Mac/Linux)
This flow builds **device-target (`larch64`) binaries on your laptop** using a Linux/aarch64 container and a synced comma sysroot.
For the full StarPilot branch workflow, including host-native shorthand tools such as `./dev`, `./c3`, `./c4`, and `./raybig`, see [tools/STARPILOT_DEVELOPMENT.md](../../tools/STARPILOT_DEVELOPMENT.md).
## Prerequisites
- Docker Desktop (or Podman) with Linux/aarch64 support.
- `rsync` and `ssh` on your laptop.
- Either:
- access to a comma device over SSH, or
- internet access to download AGNOS system image for sysroot extraction.
## One-time setup
Fast path (no physical comma):
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh setup
```
Equivalent wrapper:
```bash
scripts/starpilot_build_flow.sh laptop-setup
```
### Option A: no physical comma (AGNOS-based)
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh build-image
scripts/laptop_device_build.sh setup-sysroot-agnos
```
### Option B: copy sysroot from a comma device
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh setup-sysroot <device-ip> comma 22
scripts/laptop_device_build.sh build-image
```
Optional all-in-one with physical device sysroot:
```bash
scripts/laptop_device_build.sh setup <device-ip> comma 22
```
## Build device-compatible artifacts
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh build
```
Equivalent wrapper:
```bash
scripts/starpilot_build_flow.sh laptop-device
```
This runs:
- `uv sync` into `/work/.venv-linux-arm64` (inside container)
- full `scons` with:
- `SP_FORCE_ARCH=larch64`
- `SP_FORCE_TICI=1`
- `SP_TICI_SYSROOT=/opt/tici-sysroot`
- `touch prebuilt`
To run `scons` targets explicitly in the same device-compatible environment:
```bash
scripts/laptop_device_build.sh scons selfdrive/ui/ui
```
On macOS, once `.comma_sysroot` is present, plain `scons ...` auto-routes to this containerized device build.
Set `SP_DISABLE_AUTO_DEVICE_SCONS=1` to force native host `scons`.
## Quick checks
```bash
scripts/laptop_device_build.sh doctor
```
If `doctor` fails, fix the missing runtime/sysroot step before running `build`.
## One-command manager launch on desktop
`./launch_openpilot.sh` now auto-routes desktop/mac launches to the containerized larch64 manager path:
- runs `scripts/laptop_device_build.sh doctor`
- runs `setup` automatically if prerequisites are missing
- runs container manager launch, auto-building missing runtime artifacts first
Useful overrides:
- `SP_SKIP_DOCKER_AUTO_BUILD=1 ./launch_openpilot.sh` to skip auto-build.
- `SP_DOCKER_BUILD_JOBS=12 ./launch_openpilot.sh` to control build parallelism.
- `SP_FORCE_DEVICE_LAUNCH=1 ./launch_openpilot.sh` to force legacy device launch path.
Note:
- `manager` mode requires the container runtime machine to be `aarch64`.
- If your built image runs as `x86_64`, build mode still works for artifact generation, but manager launch in-container is not supported.
Preferred host-side shorthand commands on this branch:
```bash
./c3
./c4
./raybig
./dev replay
./dev cabana
./dev plotjuggler
```
These commands use the isolated `.host_runtime` cache so host-native artifacts do not churn the main tree.
Legacy direct script entrypoints still exist if needed:
```bash
scripts/launch_ui_desktop.sh
scripts/launch_ui_c4_desktop.sh
scripts/launch_ui_raybig_desktop.sh
```
-304
View File
@@ -1,304 +0,0 @@
# Train Speed-Limit Vision
This flow is for replacing the current imported `ayoubsa_best` checkpoint with a U.S.-focused detector plus a separate posted-speed classifier.
The intended recipe is:
1. bootstrap from public traffic-sign data
2. fine-tune on comma-specific bookmarked drives
3. export ONNX models
4. copy them onto the device for runtime testing
## Why Two Models
The detector and the value reader solve different problems.
- detector: find the sign and decide whether it is a regulatory speed-limit sign
- classifier: read the posted value from the cropped sign
This is a better fit for U.S. roads than a single detector with baked-in classes like `Speed Limit 10`, `Speed Limit 20`, `Speed Limit 30`, and so on.
## Suggested Public Data
- `LISA` for U.S. roadside sign geometry and sign style
- `ARTS` for U.S. MUTCD-style sign annotations and values
- `GLARE` for U.S. glare/lighting failures
Use public data to get the model into the right regime, then fine-tune on comma bookmarks and replay clips.
## Install Training Deps
From the repo root:
```bash
uv sync --extra speedvision
```
The runtime device does not need these packages. Only the training machine does.
## Initialize a Workspace
```bash
.venv/bin/python scripts/speed_limit_vision/init_workspace.py
```
This creates `.tmp/speed_limit_training` with:
- detector image/label folders
- classifier crop folders
- review/bookmark manifests
- raw-source manifests
- export and run directories
To keep the raw datasets off the internal disk, point `--workspace` at the SSD-backed workspace, for example:
```bash
.venv/bin/python scripts/speed_limit_vision/init_workspace.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean
```
The workspace now also tracks:
- `manifests/raw_sources.csv`
- `manifests/public_detector_samples.csv`
- `manifests/public_classifier_samples.csv`
Those manifests are the provenance record for every imported public sample.
## Import Public Datasets
ARTS challenging subset:
```bash
.venv/bin/python scripts/speed_limit_vision/import_arts_challenging.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean
```
This imports mapped speed-limit signs from the raw `challenging-dev.tar.gz` archive and appends detector/classifier provenance rows into the manifest CSVs.
GLARE and LISA should be downloaded into the SSD raw tree first:
- `/Volumes/T5/starpilot_speed_limit/raw/glare_official`
- `/Volumes/T5/starpilot_speed_limit/raw/lisa_official`
Then import them into the same workspace so the detector/classifier datasets stay source-traceable.
For GLARE, do not pull the whole Drive tree blindly. Use the filtered raw downloader so only the `Images/` and optional `Tracks/` files land on disk:
```bash
.venv/bin/python scripts/speed_limit_vision/download_glare_raw.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean \
--output-root /Volumes/T5/starpilot_speed_limit/raw/glare_raw \
--prefix Images/ \
--resume
```
Then import the completed image tree:
```bash
.venv/bin/python scripts/speed_limit_vision/import_glare_images.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean \
--overwrite
```
## Import Bookmarked Debug Sessions
After a drive, copy or mount the debug session directory locally, then import it:
```bash
.venv/bin/python scripts/speed_limit_vision/import_debug_sessions.py
```
Or point at specific sessions:
```bash
.venv/bin/python scripts/speed_limit_vision/import_debug_sessions.py 20260330_220102 20260330_223355
```
This writes:
- `review/bookmarks.csv`
- snapshot images under `review/images`
That manifest is the shortlist for labeling.
If the route only lives on comma connect, fetch it directly into the same clip layout the bookmark tools expect:
```bash
.venv/bin/python scripts/speed_limit_vision/download_connect_routes.py \
<dongle_id>/<route_log_id> \
--streams fcamera,qlog
```
This reads the JWT from `~/.comma/auth.json`, downloads the requested route files from comma connect, and updates:
- `/Volumes/T5/starpilot_speed_limit/live_route_clips/bookmark_windows/data/media/0/realdata/...` when the SSD is mounted, otherwise the same `.tmp/...` paths under the repo
- `/Volumes/T5/starpilot_speed_limit/live_routes_meta/qlog_mtimes.txt` when the SSD is mounted, otherwise `.tmp/live_routes_meta/qlog_mtimes.txt`
- `/Volumes/T5/starpilot_speed_limit/live_routes_meta/files.txt` when the SSD is mounted, otherwise `.tmp/live_routes_meta/files.txt`
For multiple routes, either pass several ids on the command line or use `--routes-file`.
## Evaluate Real Sign Lead-Ins
Bookmark stills are often too late. To score what matters, replay the real `fcamera.hevc` footage from the `5` seconds before each bookmark:
```bash
.venv/bin/python scripts/speed_limit_vision/evaluate_bookmark_leadins.py \
--json-out .tmp/live_route_clips/bookmark_windows_report.json
```
This produces a per-bookmark report of whether the current runtime saw anything in the usable sign approach window.
The evaluator now reads a local session-to-route map from `session_route_map.json` under the same `live_routes_meta` root. Keep that file local or on the SSD so personal route ids never need to live in the repo.
## Import Missed Lead-Ins for Labeling
Turn those lead-in misses into review frames and contact sheets:
```bash
.venv/bin/python scripts/speed_limit_vision/import_bookmark_leadins.py \
--mode misses
```
This writes:
- `review/bookmark_leadins.csv`
- sampled frames under `review/leadins/frames`
- contact sheets under `review/leadins/contact_sheets`
That review set is the right source for labeling missed `55 mph`, night, and town-sequence failures.
The bookmark/lead-in importers also accept source metadata fields such as region, device, and driver. Use those when importing debug sessions from multiple users so the comma-specific fine-tune can be sliced by contributor or geography instead of becoming one opaque pool.
To shrink that review set to the most promising frames per missed sign window:
```bash
.venv/bin/python scripts/speed_limit_vision/rank_bookmark_leadin_frames.py
```
This writes `review/bookmark_leadin_shortlist.csv` with the top-ranked sampled frames per bookmark.
## Build the Detector Dataset
Take the imported review images and move or copy the ones you want into:
- `detector/images/train`
- `detector/images/val`
Label them in YOLO detect format into:
- `detector/labels/train`
- `detector/labels/val`
Recommended classes:
- `regulatory_speed_limit`
- `advisory_speed_limit`
- `school_zone_speed_limit`
The dataset YAML is already generated at:
- `detector/dataset.yaml`
## Build the Value Classifier Dataset
Fill out:
- `classifier/value_labels.csv`
Columns:
- `image_path`: source image file
- `split`: `train` or `val`
- `speed_limit_mph`: posted value such as `25`, `35`, or `55`
- `bbox_index`: which YOLO box to crop if an image has multiple labeled signs
- `padding`: optional crop padding ratio
- `label_path`: optional explicit path to the YOLO label file
Then generate the classifier crop folders:
```bash
.venv/bin/python scripts/speed_limit_vision/build_value_dataset.py
```
This writes cropped sign images into:
- `classifier/train/<value>/...`
- `classifier/val/<value>/...`
## Train
Detector:
```bash
.venv/bin/python scripts/speed_limit_vision/train_detector.py --device mps
```
Classifier:
```bash
.venv/bin/python scripts/speed_limit_vision/train_value_classifier.py --device mps
```
Use `--device cpu`, `--device mps`, or a CUDA device string depending on the training host.
## Rebalance Toward Real Comma Data
If the detector starts overfitting to synthetic/public data, build a lighter rebalanced dataset that keeps all `real_*` detector images and samples the rest:
```bash
.venv/bin/python scripts/speed_limit_vision/rebalance_detector_dataset.py \
--workspace .tmp/speed_limit_training \
--max-other-train 3200
```
Then point the detector trainer at the generated YAML:
```bash
.venv/bin/python scripts/speed_limit_vision/train_detector.py \
--workspace .tmp/speed_limit_training \
--data .tmp/speed_limit_training/detector_rebalanced/dataset.yaml \
--device mps
```
This keeps validation unchanged while making retrains faster and more comma-biased.
## Export ONNX
```bash
.venv/bin/python scripts/speed_limit_vision/export_models.py \
--detector-weights .tmp/speed_limit_training/runs/detector/yolo11n-speed-limit-us/weights/best.pt \
--classifier-weights .tmp/speed_limit_training/runs/classifier/yolo11n-cls-speed-limit-us/weights/best.pt \
--install-repo-assets
```
That writes:
- `.tmp/speed_limit_training/exports/speed_limit_us_detector.onnx`
- `.tmp/speed_limit_training/exports/speed_limit_us_value_classifier.onnx`
And optionally copies them into:
- `starpilot/assets/vision_models`
## Copy to the Device
```bash
.venv/bin/python scripts/speed_limit_vision/install_models.py --host comma@192.168.3.110
```
The runtime already prefers `speed_limit_us_detector.onnx` plus `speed_limit_us_value_classifier.onnx` when both files exist in `starpilot/assets/vision_models`.
## Evaluate the Runtime Path
Run the real StarPilot runtime path, using the installed ONNX pair, against the known saved-frame cases:
```bash
.venv/bin/python scripts/speed_limit_vision/evaluate_runtime_cases.py --strict
```
For temporal behavior on a saved frame directory or route extract, replay the runtime directly:
```bash
.venv/bin/python scripts/replay_speed_limit_vision.py .tmp/vision_iter/seg10_5fps --frames-fps 5
```
View File
+598
View File
@@ -0,0 +1,598 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dashy State Aggregation Daemon
Aggregates all cereal topics needed by dashy UI into a single dashyState message.
This reduces CPU overhead in webrtcd by doing JSON serialization once here
instead of serializing 14+ topics separately.
All display formatting (units, distances, times) is done here so the frontend
can be a pure display layer with no conversion logic.
Publishes: dashyState (pre-serialized JSON at 15Hz)
"""
import json
import cereal.messaging as messaging
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
from opendbc.car.common.conversions import Conversions
# Main loop rate
LOOP_RATE = 15 # Hz
# Downsample factor for modelV2 arrays (33 points -> 17 points)
DOWNSAMPLE_FACTOR = 2
# Unit conversion constants
M_TO_KM = 0.001
M_TO_MI = 0.000621371
M_TO_FT = 3.28084
# Global state (refreshed periodically)
_is_metric = True
_params = None
_car_params_cache = None
def _ensure_params():
"""Ensure Params instance exists."""
global _params
if _params is None:
_params = Params()
return _params
def refresh_metric_preference():
"""Refresh metric preference from params (called periodically)."""
global _is_metric
try:
_is_metric = _ensure_params().get_bool("IsMetric")
except Exception:
_is_metric = True
def get_car_params_from_params():
"""Read carParams from Params storage (for immediate availability at startup)."""
global _car_params_cache
if _car_params_cache is not None:
return _car_params_cache
try:
from cereal import car
cp_bytes = _ensure_params().get("CarParams")
if cp_bytes:
with car.CarParams.from_bytes(cp_bytes) as cp:
_car_params_cache = {
'openpilotLongitudinalControl': bool(cp.openpilotLongitudinalControl),
}
return _car_params_cache
except Exception:
pass
return {'openpilotLongitudinalControl': False}
def format_speed(speed_ms: float) -> str:
"""Format speed for display (m/s -> km/h or mph)."""
if _is_metric:
return f"{max(0, speed_ms * Conversions.MS_TO_KPH):.0f}"
return f"{max(0, speed_ms * Conversions.MS_TO_MPH):.0f}"
def format_speed_value(speed_ms: float) -> float:
"""Convert speed to display units (m/s -> km/h or mph)."""
if _is_metric:
return max(0, speed_ms * Conversions.MS_TO_KPH)
return max(0, speed_ms * Conversions.MS_TO_MPH)
def format_distance(meters: float) -> str:
"""Format distance for display."""
if meters <= 0:
return ""
if _is_metric:
if meters >= 1000:
return f"{meters * M_TO_KM:.1f} km"
return f"{meters:.0f} m"
else:
miles = meters * M_TO_MI
if miles >= 0.1:
return f"{miles:.1f} mi"
return f"{meters * M_TO_FT:.0f} ft"
def format_time(seconds: float) -> str:
"""Format time duration for display."""
if seconds <= 0:
return ""
minutes = int(seconds / 60)
if minutes < 60:
return f"{minutes} min"
hours = minutes // 60
mins = minutes % 60
if mins == 0:
return f"{hours} hr"
return f"{hours} hr {mins} min"
def get_speed_unit() -> str:
"""Get current speed unit string."""
return "km/h" if _is_metric else "mph"
def get_distance_unit() -> str:
"""Get current distance unit string."""
return "km" if _is_metric else "mi"
SET_SPEED_NA = 255
def get_cruise_speed(v_cruise_cluster: float) -> int:
"""Get cruise speed value for display.
Returns the set speed in display units (km/h or mph), or 255 if not set.
"""
if not (0 < v_cruise_cluster < SET_SPEED_NA):
return SET_SPEED_NA
set_speed = v_cruise_cluster
if not _is_metric:
set_speed *= Conversions.KPH_TO_MPH
return round(set_speed)
def downsample(arr):
"""Downsample list by factor."""
if not arr:
return []
return list(arr[::DOWNSAMPLE_FACTOR])
def safe_get(obj, attr, default=None):
"""Safely get attribute from object."""
try:
return getattr(obj, attr, default)
except Exception:
return default
def extract_car_state(sm):
"""Extract carState fields used by dashy."""
cs = sm['carState']
v_ego = float(cs.vEgo)
v_ego_cluster = float(cs.vEgoCluster)
# Get set speed from controlsState.vCruiseDEPRECATED (fallback to carState.vCruiseCluster)
v_cruise = float(cs.vCruiseCluster)
if 'controlsState' in sm.updated and sm.updated['controlsState']:
v_cruise = float(sm['controlsState'].vCruiseDEPRECATED)
set_speed = get_cruise_speed(v_cruise)
return {
'vEgo': v_ego,
'vEgoCluster': v_ego_cluster,
'gearShifter': str(cs.gearShifter),
'aEgo': float(cs.aEgo),
'steeringAngleDeg': float(cs.steeringAngleDeg),
'leftBlinker': bool(cs.leftBlinker),
'rightBlinker': bool(cs.rightBlinker),
'leftBlindspot': bool(cs.leftBlindspot),
'rightBlindspot': bool(cs.rightBlindspot),
# Pre-formatted display values
'speedDisplay': format_speed(v_ego),
'speedClusterDisplay': format_speed(v_ego_cluster) if v_ego_cluster > 0 else format_speed(v_ego),
'setSpeed': set_speed, # 255 = not set, otherwise display value
'speedUnit': get_speed_unit(),
}
def extract_selfdrive_state(sm):
"""Extract selfdriveState fields used by dashy."""
ss = sm['selfdriveState']
return {
'enabled': bool(ss.enabled),
'activeOverride': int(safe_get(ss, 'activeOverride', 0)),
'experimentalMode': bool(ss.experimentalMode),
'alertText1': str(ss.alertText1),
'alertText2': str(ss.alertText2),
'alertSize': str(ss.alertSize),
'alertStatus': str(ss.alertStatus),
}
def extract_device_state(sm):
"""Extract deviceState fields used by dashy."""
ds = sm['deviceState']
temp_c = float(safe_get(ds, 'maxTempC', 0))
# Pre-format temperature for display
if _is_metric:
temp_display = f"{temp_c:.0f}°" if temp_c > 0 else "--"
else:
temp_f = temp_c * 9 / 5 + 32
temp_display = f"{temp_f:.0f}°" if temp_c > 0 else "--"
return {
'cpuUsagePercent': list(ds.cpuUsagePercent) if ds.cpuUsagePercent else [],
'memoryUsagePercent': int(ds.memoryUsagePercent),
'maxTempC': temp_c,
'deviceType': str(ds.deviceType),
'tempDisplay': temp_display,
}
def extract_lead(lead, sm):
"""Extract lead vehicle data."""
d_rel = float(lead.dRel)
v_rel = float(lead.vRel)
y_rel = float(lead.yRel)
has_lead = bool(lead.status)
# Pre-format lead display values
if has_lead:
v_ego = float(sm['carState'].vEgo) if sm.valid['carState'] else 0
lead_speed_ms = max(0, v_ego + v_rel)
speed_display = format_speed(lead_speed_ms)
distance_display = f"{d_rel:.1f}m" if _is_metric else f"{d_rel * M_TO_FT:.0f}ft"
else:
speed_display = "--"
distance_display = "--"
return {
'status': has_lead,
'dRel': d_rel,
'yRel': y_rel,
'vRel': v_rel,
'speedDisplay': speed_display,
'distanceDisplay': distance_display,
}
def extract_radar_state(sm):
"""Extract radarState fields used by dashy."""
rs = sm['radarState']
return {
'leadOne': extract_lead(rs.leadOne, sm),
'leadTwo': extract_lead(rs.leadTwo, sm),
}
def extract_live_tracks(sm):
"""Extract liveTracks radar points for bird's eye view.
Filters out tracks that are already shown as leadOne or leadTwo.
Uses radarTrackId matching: when radarState matches a liveTrack to a lead,
radarTrackId changes from -1 (vision-only) to the track's ID.
"""
try:
lt = sm['liveTracks']
points = []
# Get lead vehicle radar track IDs to filter them out
# radarTrackId = -1 means vision-only (no radar match)
# radarTrackId >= 0 means matched to a radar track
lead_track_ids = set()
if sm.valid.get('radarState', False):
rs = sm['radarState']
if rs.leadOne.status and rs.leadOne.radarTrackId >= 0:
lead_track_ids.add(rs.leadOne.radarTrackId)
if rs.leadTwo.status and rs.leadTwo.radarTrackId >= 0:
lead_track_ids.add(rs.leadTwo.radarTrackId)
if hasattr(lt, 'points'):
for pt in lt.points:
# Skip if this track is already shown as a lead vehicle
if pt.trackId in lead_track_ids:
continue
points.append({
'd': float(pt.dRel),
'y': float(pt.yRel),
'v': float(pt.vRel),
'm': bool(pt.measured),
})
return {'points': points}
except Exception as e:
cloudlog.warning(f"extract_live_tracks error: {e}")
return {'points': []}
def extract_live_gps(sm):
"""Extract liveGPS fields used by dashy."""
gps = sm['liveGPS']
# Skip if no coordinates yet
if gps.latitude == 0 and gps.longitude == 0:
return None
return {
'latitude': float(gps.latitude),
'longitude': float(gps.longitude),
'bearingDeg': float(gps.bearingDeg),
'speed': float(gps.speed),
'gpsOK': bool(gps.gpsOK),
'horizontalAccuracy': float(gps.horizontalAccuracy),
'status': str(gps.status),
}
def extract_nav_instruction(sm):
"""Extract navInstruction fields used by dashy."""
nav = sm['navInstruction']
maneuver_dist = float(safe_get(nav, 'maneuverDistance', 0))
dist_remaining = float(safe_get(nav, 'distanceRemaining', 0))
time_remaining = float(safe_get(nav, 'timeRemaining', 0))
speed_limit_ms = float(safe_get(nav, 'speedLimit', 0))
return {
'valid': sm.valid['navInstruction'],
'maneuverDistance': maneuver_dist,
'maneuverPrimaryText': str(safe_get(nav, 'maneuverPrimaryText', '')),
'maneuverSecondaryText': str(safe_get(nav, 'maneuverSecondaryText', '')),
'maneuverType': str(safe_get(nav, 'maneuverType', '')),
'maneuverModifier': str(safe_get(nav, 'maneuverModifier', '')),
'distanceRemaining': dist_remaining,
'timeRemaining': time_remaining,
'timeRemainingTypical': float(safe_get(nav, 'timeRemainingTypical', 0)),
'speedLimit': speed_limit_ms,
'speedLimitSign': str(safe_get(nav, 'speedLimitSign', '')),
# Pre-formatted display values
'maneuverDistanceDisplay': format_distance(maneuver_dist),
'distanceRemainingDisplay': format_distance(dist_remaining),
'timeRemainingDisplay': format_time(time_remaining),
'speedLimitDisplay': format_speed(speed_limit_ms) if speed_limit_ms > 0 else '',
}
def extract_nav_instruction_ext(sm):
"""Extract navInstructionExt fields used by dashy (extended nav data)."""
nav_ext = sm['navInstructionExt']
# Extract allManeuvers list (with name field and formatted distance)
all_maneuvers = []
if hasattr(nav_ext, 'allManeuvers'):
for m in nav_ext.allManeuvers:
dist = float(safe_get(m, 'distance', 0))
all_maneuvers.append({
'distance': dist,
'distanceDisplay': format_distance(dist),
'type': str(safe_get(m, 'type', '')),
'modifier': str(safe_get(m, 'modifier', '')),
'name': str(safe_get(m, 'name', '')),
})
return {
'turnAngle': float(safe_get(nav_ext, 'turnAngle', 0)),
'turnCurvature': float(safe_get(nav_ext, 'turnCurvature', 0)),
'allManeuvers': all_maneuvers,
}
def extract_nav_route(sm):
"""Extract navRoute coordinates used by dashy."""
route = sm['navRoute']
coords = []
if hasattr(route, 'coordinates'):
for c in route.coordinates:
coords.append([float(c.longitude), float(c.latitude)])
return {
'coordinates': coords,
}
def extract_model_v2(sm):
"""Extract modelV2 fields used by dashy (downsampled)."""
model = sm['modelV2']
# Position
pos = model.position
position = {
'x': downsample(list(pos.x)),
'y': downsample(list(pos.y)),
'z': downsample(list(pos.z)),
}
# Lane lines (4 lines)
lane_lines = []
for line in model.laneLines:
lane_lines.append({
'x': downsample(list(line.x)),
'y': downsample(list(line.y)),
'z': downsample(list(line.z)),
})
# Road edges (2 edges)
road_edges = []
for edge in model.roadEdges:
road_edges.append({
'x': downsample(list(edge.x)),
'y': downsample(list(edge.y)),
'z': downsample(list(edge.z)),
})
return {
'position': position,
'laneLines': lane_lines,
'laneLineProbs': list(model.laneLineProbs) if hasattr(model, 'laneLineProbs') else [0, 0, 0, 0],
'roadEdges': road_edges,
'roadEdgeStds': list(model.roadEdgeStds) if hasattr(model, 'roadEdgeStds') else [1, 1],
}
def extract_live_calibration(sm):
"""Extract liveCalibration fields used by dashy."""
cal = sm['liveCalibration']
return {
'rpyCalib': list(cal.rpyCalib) if hasattr(cal, 'rpyCalib') and cal.rpyCalib else [],
'calStatus': str(cal.calStatus) if hasattr(cal, 'calStatus') else 'uncalibrated',
'height': list(cal.height) if hasattr(cal, 'height') else [],
}
def extract_longitudinal_plan(sm):
"""Extract longitudinalPlan fields used by dashy."""
lp = sm['longitudinalPlan']
return {
'allowThrottle': bool(safe_get(lp, 'allowThrottle', True)),
}
def extract_controls_state_ext(sm):
"""Extract controlsStateExt fields used by dashy."""
cse = sm['controlsStateExt']
return {
'alkaActive': bool(safe_get(cse, 'alkaActive', False)),
}
def extract_car_params(sm):
"""Extract carParams fields used by dashy."""
cp = sm['carParams']
return {
'openpilotLongitudinalControl': bool(safe_get(cp, 'openpilotLongitudinalControl', False)),
}
# =============================================================================
# TOPIC CONFIGURATION
# =============================================================================
# Single source of truth for all subscribed topics.
# Comment out a line to disable that topic entirely.
#
# Fields:
# extractor: function(sm) -> dict, extracts data from message
# rate: 'fast' = every frame when updated
# number = slow poll divider (e.g., LOOP_RATE = 1Hz)
# 'valid' = just track valid state, no extraction
# 'subscribe' = subscribed but extracted within other extractors
# default: initial cache value (None if not specified)
# =============================================================================
TOPICS = {
# Fast topics - extract every frame when updated
'carState': {'extractor': extract_car_state, 'rate': 'fast'},
'selfdriveState': {'extractor': extract_selfdrive_state, 'rate': 'fast'},
'radarState': {'extractor': extract_radar_state, 'rate': 'fast'},
'liveTracks': {'extractor': extract_live_tracks, 'rate': 'fast'},
'modelV2': {'extractor': extract_model_v2, 'rate': 'fast'},
'longitudinalPlan': {'extractor': extract_longitudinal_plan, 'rate': 'fast'},
'liveGPS': {'extractor': extract_live_gps, 'rate': 'fast'},
# Slow topics - poll at fixed intervals
'deviceState': {'extractor': extract_device_state, 'rate': LOOP_RATE // 2},
'liveCalibration': {'extractor': extract_live_calibration, 'rate': LOOP_RATE},
'navInstruction': {'extractor': extract_nav_instruction, 'rate': LOOP_RATE},
'navInstructionExt': {'extractor': extract_nav_instruction_ext, 'rate': LOOP_RATE},
'navRoute': {'extractor': extract_nav_route, 'rate': LOOP_RATE},
'carParams': {'extractor': extract_car_params, 'rate': LOOP_RATE * 2},
# Valid-only topics - just track valid state
'roadCameraState': {'rate': 'valid', 'default': False},
# Subscribe-only topics - subscribed but extracted within other extractors
'controlsState': {'rate': 'subscribe'},
# Optional/dragonpilot-specific topics - comment out to disable
'controlsStateExt': {'extractor': extract_controls_state_ext, 'rate': 'fast', 'default': {'alkaActive': False}},
}
def main():
cloudlog.info("dashyd: starting")
# Initialize metric preference
refresh_metric_preference()
# Derive services list from TOPICS config
services = list(TOPICS.keys())
sm = messaging.SubMaster(services)
pm = messaging.PubMaster(['dashyState'])
rk = Ratekeeper(LOOP_RATE)
# Initialize cache from TOPICS defaults (exclude subscribe-only topics)
cache = {t: cfg.get('default') for t, cfg in TOPICS.items() if cfg.get('rate') != 'subscribe'}
cache['carParams'] = get_car_params_from_params() # special: init from Params
# Build topic lists from TOPICS config
fast_topics = {t: cfg['extractor'] for t, cfg in TOPICS.items() if cfg.get('rate') == 'fast'}
slow_topics = {t: (cfg['extractor'], cfg['rate']) for t, cfg in TOPICS.items()
if isinstance(cfg.get('rate'), int)}
valid_topics = [t for t, cfg in TOPICS.items() if cfg.get('rate') == 'valid']
cache_dirty = True
frame_count = 0
while True:
sm.update(0)
frame_count += 1
# Refresh metric preference every ~2 seconds
if frame_count % (LOOP_RATE * 2) == 0:
refresh_metric_preference()
cache_dirty = True # Force re-format with new units
# Fast topics - extract when updated
for topic, extractor in fast_topics.items():
if sm.updated[topic]:
cache[topic] = extractor(sm)
cache_dirty = True
# Slow topics - extract at fixed intervals (ignore sm.updated)
for topic, (extractor, divider) in slow_topics.items():
if frame_count % divider == 0:
cache[topic] = extractor(sm)
cache_dirty = True
# Valid-only topics - just track valid state
for topic in valid_topics:
if sm.updated[topic]:
new_val = sm.valid[topic]
if cache[topic] != new_val:
cache[topic] = new_val
cache_dirty = True
# Only serialize and publish if something changed
if cache_dirty:
# Only publish when critical data exists (nav data can be null)
critical_ready = (
cache.get('carState') is not None and
cache.get('modelV2') is not None and
cache.get('selfdriveState') is not None
)
if critical_ready:
state = {
'ts': sm.logMonoTime['carState'],
'display': {
'isMetric': _is_metric,
'speedUnit': get_speed_unit(),
'distanceUnit': get_distance_unit(),
},
**cache, # include all cached topics
}
msg = messaging.new_message('dashyState')
msg.dashyState.json = json.dumps(state).encode()
pm.send('dashyState', msg)
cache_dirty = False
rk.keep_time()
if __name__ == "__main__":
main()
+423
View File
@@ -0,0 +1,423 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
GPS Location Service - GPS + livePose fusion with Kalman filter.
- Position: 2D Kalman filter fusing GPS with livePose velocity
- Bearing: livePose yaw + GPS-calibrated offset (with slow drift correction)
"""
import numpy as np
from cereal import messaging, custom
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.transformations.coordinates import LocalCoord
from openpilot.common.swaglog import cloudlog
from openpilot.common.gps import get_gps_location_service
def wrap_angle(x):
return np.arctan2(np.sin(x), np.cos(x))
class GPSKalman:
"""
3D Kalman filter for GPS fusion.
State: [north, east, yaw_offset] where yaw_offset calibrates livePose to true north.
Adapts automatically to GPS accuracy (ublox vs qcom).
"""
# Process noise
POS_NOISE = 0.5 # m²/s - position uncertainty growth
YAW_NOISE = 0.0001 # rad²/s - yaw offset drift (~0.6°/min)
def __init__(self):
self.x = np.zeros(3) # [north, east, yaw_offset]
self.P = np.diag([100.0, 100.0, 1.0]) # uncertainty (position high, yaw moderate)
def get_yaw(self, pose_yaw):
"""Get calibrated yaw from pose yaw + estimated offset."""
return wrap_angle(pose_yaw + self.x[2])
def predict(self, vel_ned, dt):
"""Predict state using velocity from livePose."""
# Position prediction
self.x[0] += vel_ned[0] * dt
self.x[1] += vel_ned[1] * dt
# yaw_offset: no change (constant), just add process noise
# Process noise - more yaw drift when stopped (gyro drift accumulates)
speed = np.linalg.norm(vel_ned[:2])
yaw_noise = self.YAW_NOISE if speed > 1.0 else self.YAW_NOISE * 10
Q = np.diag([self.POS_NOISE * dt, self.POS_NOISE * dt, yaw_noise * dt])
self.P += Q
def update_position(self, gps_ned, gps_accuracy):
"""Update position with GPS measurement."""
# Observation matrix: observe [north, east], not yaw_offset
H = np.array([[1, 0, 0],
[0, 1, 0]])
# Measurement noise from GPS accuracy
R = np.eye(2) * (gps_accuracy ** 2)
# Innovation
z = gps_ned[:2]
y = z - H @ self.x
# Kalman gain
S = H @ self.P @ H.T + R
det = S[0, 0] * S[1, 1] - S[0, 1] * S[1, 0]
if abs(det) < 1e-10:
return
S_inv = np.array([[S[1, 1], -S[0, 1]],
[-S[1, 0], S[0, 0]]]) / det
K = self.P @ H.T @ S_inv
# Update
self.x += K @ y
self.P = (np.eye(3) - K @ H) @ self.P
self._ensure_positive_definite()
def update_yaw(self, gps_bearing, pose_yaw, bearing_std):
"""Update yaw_offset with GPS bearing measurement."""
# Observation: yaw_offset = gps_bearing - pose_yaw
# H = [0, 0, 1] - we observe yaw_offset directly
H = np.array([[0, 0, 1]])
# Measurement noise from GPS bearing uncertainty
R = np.array([[bearing_std ** 2]])
# Expected yaw_offset from GPS
observed_offset = wrap_angle(gps_bearing - pose_yaw)
# Innovation (handle angle wrapping)
predicted_offset = self.x[2]
y = np.array([wrap_angle(observed_offset - predicted_offset)])
# Kalman gain
S = H @ self.P @ H.T + R
K = self.P @ H.T / S[0, 0]
# Update
self.x += (K @ y).flatten()
self.x[2] = wrap_angle(self.x[2]) # keep yaw_offset wrapped
self.P = (np.eye(3) - K @ H) @ self.P
self._ensure_positive_definite()
def _ensure_positive_definite(self):
"""Ensure covariance stays positive definite. Reinit if corrupted."""
self.P = (self.P + self.P.T) / 2
if np.any(np.diag(self.P) < 0):
cloudlog.warning("gpsd: negative covariance detected, reinitializing filter")
self.P = np.diag([100.0, 100.0, 1.0])
return
min_var = np.array([0.01, 0.01, 0.0001]) # minimum variances
for i in range(3):
self.P[i, i] = max(self.P[i, i], min_var[i])
def reset(self, pos, yaw_offset=None):
"""Reset to known position, optionally with yaw offset."""
self.x[0] = pos[0]
self.x[1] = pos[1]
if yaw_offset is not None:
self.x[2] = yaw_offset
self.P = np.diag([1.0, 1.0, 0.1]) # low uncertainty
else:
self.P = np.diag([1.0, 1.0, self.P[2, 2]]) # keep yaw uncertainty
@property
def pos(self):
"""Position [north, east]."""
return self.x[:2]
@property
def yaw_offset(self):
"""Yaw offset estimate."""
return self.x[2]
@property
def pos_uncertainty(self):
"""Position uncertainty (meters)."""
return np.sqrt(max(0.0, (self.P[0, 0] + self.P[1, 1]) / 2))
@property
def yaw_uncertainty(self):
"""Yaw offset uncertainty (radians)."""
return np.sqrt(max(0.0, self.P[2, 2]))
class LiveGPS:
"""
GPS + livePose fusion with 3D Kalman filter.
- Position: Kalman filter fusing GPS with livePose velocity
- Bearing: Kalman-estimated yaw_offset + livePose yaw
"""
GPS_MIN_SPEED = 5.0 # m/s (18 km/h) - need speed for reliable GPS bearing
GPS_MAX_ACCURACY = 50.0 # m - reject very bad GPS
BEARING_STD_BASE = 0.1 # rad (~6°) - base GPS bearing uncertainty
BEARING_STD_PER_ACC = 0.02 # rad per meter of GPS accuracy
def __init__(self):
# pose inputs
self.orientation_ned = np.zeros(3)
self.vel_device = np.zeros(3)
# gps inputs
self.gps = None
self.last_gps_t = 0.0
self.unix_timestamp_millis = 0
# Kalman filter: [north, east, yaw_offset]
self.origin = None # LocalCoord of first GPS fix
self.kf = GPSKalman() # 3D Kalman filter
self.altitude = 0.0 # altitude tracked separately (1D)
self.last_gps_update_t = 0.0 # track when we last updated Kalman with GPS
# timing
self.last_t = None
self.last_pose_yaw = None # for yaw rate calculation
self.live_pose_ok = False # for monitoring
# -----------------------------
# inputs
# -----------------------------
def handle_pose(self, pose):
if pose.orientationNED.valid:
self.orientation_ned[:] = [
pose.orientationNED.x,
pose.orientationNED.y,
pose.orientationNED.z,
]
if pose.velocityDevice.valid:
self.vel_device[:] = [
pose.velocityDevice.x,
pose.velocityDevice.y,
pose.velocityDevice.z,
]
# For monitoring
self.live_pose_ok = pose.orientationNED.valid and pose.velocityDevice.valid
def handle_gps(self, t, gps):
if gps.horizontalAccuracy > 0 and gps.horizontalAccuracy > self.GPS_MAX_ACCURACY:
return
if abs(gps.latitude) < 0.1 or abs(gps.longitude) < 0.1:
return
self.gps = gps
self.last_gps_t = t
self.unix_timestamp_millis = gps.unixTimestampMillis
# -----------------------------
# core update
# -----------------------------
def update(self, t):
dt = (t - self.last_t) if self.last_t else 0.05
self.last_t = t
if self.gps is None:
return
# initialize origin on first GPS
if self.origin is None:
self.origin = LocalCoord.from_geodetic([self.gps.latitude, self.gps.longitude, self.gps.altitude])
self.kf.reset(np.zeros(2))
self.altitude = self.gps.altitude
cloudlog.info(f"gpsd: origin set at {self.gps.latitude:.6f}, {self.gps.longitude:.6f}")
return
# get current yaw from Kalman (pose_yaw + estimated yaw_offset)
pose_yaw = self.orientation_ned[2]
yaw = self.kf.get_yaw(pose_yaw)
# transform velocity from device frame to NED
cos_yaw, sin_yaw = np.cos(yaw), np.sin(yaw)
vel_ned = np.array([
self.vel_device[0] * cos_yaw - self.vel_device[1] * sin_yaw,
self.vel_device[0] * sin_yaw + self.vel_device[1] * cos_yaw,
-self.vel_device[2]
])
# Kalman predict: propagate position using livePose velocity
# Skip prediction when stationary (GPS wanders, IMU drifts)
# Threshold 0.1 m/s - above noise (~0.04) but catches actual stops
speed = np.linalg.norm(self.vel_device[:2])
is_moving = speed > 0.1
if is_moving:
self.kf.predict(vel_ned, dt)
# Kalman update: only on NEW GPS data (not stale)
# Skip position update when stopped - prevents GPS wander from moving position
new_gps = self.last_gps_t > self.last_gps_update_t
if new_gps and is_moving:
self.last_gps_update_t = self.last_gps_t
gps_ned = self.origin.geodetic2ned([self.gps.latitude, self.gps.longitude, self.gps.altitude])
gps_accuracy = self.gps.horizontalAccuracy if self.gps.horizontalAccuracy > 0 else 15.0
# Check for hard reset (large error after tunnel/GPS loss)
error = np.linalg.norm(gps_ned[:2] - self.kf.pos)
if error > 100.0 and gps_accuracy < 20.0:
# GPS is good but we're far off - full reset (new origin, fresh Kalman)
yaw_err_deg = np.degrees(self.kf.yaw_uncertainty)
cloudlog.warning(f"gpsd: hard reset, error={error:.1f}m, yaw_unc={yaw_err_deg:.1f}°, gps_acc={gps_accuracy:.1f}m, livePoseOk={self.live_pose_ok}")
# Reset origin to current GPS - starts fresh
self.origin = LocalCoord.from_geodetic([self.gps.latitude, self.gps.longitude, self.gps.altitude])
self.kf = GPSKalman() # fresh Kalman filter
self.kf.reset(np.zeros(2)) # position (0,0) at new origin
self.altitude = self.gps.altitude
return # skip normal update this frame
else:
# Position update - adapts to GPS accuracy:
# - ublox (2-5m): high gain, trusts GPS
# - qcom (10-30m): low gain, trusts IMU more
self.kf.update_position(gps_ned, gps_accuracy)
# simple altitude tracking (no Kalman needed)
self.altitude = 0.9 * self.altitude + 0.1 * self.gps.altitude
# Yaw update (need speed for reliable GPS bearing)
if self.gps.speed > self.GPS_MIN_SPEED:
# compute yaw rate to check if driving straight
yaw_rate = 0.0
if self.last_pose_yaw is not None and dt > 0:
yaw_rate = abs(wrap_angle(pose_yaw - self.last_pose_yaw)) / dt
# GPS bearing is unreliable during turns - increase uncertainty
gps_bearing = np.radians(self.gps.bearingDeg)
bearing_std = self.BEARING_STD_BASE + self.BEARING_STD_PER_ACC * gps_accuracy
if yaw_rate > 0.1: # turning - GPS bearing lags
bearing_std *= 3.0
self.kf.update_yaw(gps_bearing, pose_yaw, bearing_std)
self.last_pose_yaw = pose_yaw
# -----------------------------
# output
# -----------------------------
def get_msg(self, log_mono_time):
msg = messaging.new_message("liveGPS", valid=True)
msg.logMonoTime = log_mono_time
out = msg.liveGPS
t = log_mono_time * 1e-9
gps_fresh = self.gps is not None and (t - self.last_gps_t) < 5.0
pos_initialized = self.origin is not None
# yaw is calibrated when uncertainty < 0.5 rad (~30°)
yaw_calibrated = self.kf.yaw_uncertainty < 0.5
if pos_initialized:
# position from Kalman filter (NED -> geodetic)
pos_ned = np.array([self.kf.pos[0], self.kf.pos[1], self.altitude])
geodetic = self.origin.ned2geodetic(pos_ned)
out.latitude = float(geodetic[0])
out.longitude = float(geodetic[1])
out.altitude = float(geodetic[2])
out.speed = float(np.linalg.norm(self.vel_device[:2]))
# horizontalAccuracy from Kalman uncertainty
out.horizontalAccuracy = float(self.kf.pos_uncertainty)
out.verticalAccuracy = float(self.gps.verticalAccuracy) if hasattr(self.gps, 'verticalAccuracy') and self.gps.verticalAccuracy > 0 else 15.0
# bearing from Kalman (pose_yaw + estimated yaw_offset)
has_livePose = np.any(self.orientation_ned != 0)
if yaw_calibrated and has_livePose and gps_fresh:
yaw = self.kf.get_yaw(self.orientation_ned[2])
out.bearingDeg = float(np.degrees(yaw) % 360)
out.status = custom.LiveGPS.Status.valid
else:
# fallback to raw GPS bearing
out.bearingDeg = float(self.gps.bearingDeg)
out.status = custom.LiveGPS.Status.uncalibrated
elif self.gps is not None:
# have GPS but not initialized yet - pass through raw
out.latitude = float(self.gps.latitude)
out.longitude = float(self.gps.longitude)
out.altitude = float(self.gps.altitude)
out.speed = float(self.gps.speed)
out.bearingDeg = float(self.gps.bearingDeg)
out.horizontalAccuracy = float(self.gps.horizontalAccuracy) if self.gps.horizontalAccuracy > 0 else 20.0
out.status = custom.LiveGPS.Status.uncalibrated
else:
out.status = custom.LiveGPS.Status.uninitialized
# gpsOK = position is usable (bearing calibration tracked separately via status)
out.gpsOK = gps_fresh and pos_initialized
out.unixTimestampMillis = self.unix_timestamp_millis
out.lastGpsTimestamp = int(self.last_gps_t * 1e9)
# livePose health - for monitoring
out.livePoseOk = self.live_pose_ok
return msg
def main():
import os
params = Params()
# EXT=1 forces gpsLocationExternal (ublox), EXT=0 forces gpsLocation (qcom)
ext_override = os.environ.get("EXT")
if ext_override == "1":
gps_service = "gpsLocationExternal"
cloudlog.info("gpsd: EXT=1, using gpsLocationExternal (ublox)")
elif ext_override == "0":
gps_service = "gpsLocation"
cloudlog.info("gpsd: EXT=0, using gpsLocation (qcom)")
else:
gps_service = get_gps_location_service(params)
pm = messaging.PubMaster(["liveGPS"])
sm = messaging.SubMaster([gps_service, "livePose"], ignore_alive=[gps_service])
gps = LiveGPS()
rk = Ratekeeper(20)
while True:
try:
sm.update(0)
if sm.logMonoTime["livePose"] > 0:
t = sm.logMonoTime["livePose"] * 1e-9
log_mono_time = sm.logMonoTime["livePose"]
else:
log_mono_time = int(rk.frame * 1e9 / 20)
t = log_mono_time * 1e-9
if sm.updated[gps_service]:
gps.handle_gps(t, sm[gps_service])
if sm.updated["livePose"] and sm.valid["livePose"]:
gps.handle_pose(sm["livePose"])
gps.update(t)
pm.send("liveGPS", gps.get_msg(log_mono_time))
except Exception:
cloudlog.exception("gpsd: error in main loop")
rk.keep_time()
if __name__ == "__main__":
main()
+30
View File
@@ -0,0 +1,30 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dragonpilot Map-Aware Assist (MAA)
Navigation-assisted driving module that provides:
- Route tracking and turn-by-turn navigation
- Turn speed limits based on road curvature
- Blinker-based turn desire for lane changes
- Physics-based acceleration limiting
"""
+657
View File
@@ -0,0 +1,657 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Navigation helpers for dragonpilot MAA.
Coordinate math, route parsing, and curvature computation utilities.
"""
from __future__ import annotations
import json
import math
from typing import Any, cast
from opendbc.car.common.conversions import Conversions
from openpilot.common.params import Params
DIRECTIONS = ('left', 'right', 'straight')
MODIFIABLE_DIRECTIONS = ('left', 'right')
EARTH_MEAN_RADIUS = 6371007.2
# Speed unit conversions to m/s
SPEED_CONVERSIONS = {
'km/h': Conversions.KPH_TO_MS,
'mph': Conversions.MPH_TO_MS,
}
class Coordinate:
def __init__(self, latitude: float, longitude: float) -> None:
self.latitude = latitude
self.longitude = longitude
self.annotations: dict[str, float] = {}
@classmethod
def from_mapbox_tuple(cls, t: tuple[float, float]) -> Coordinate:
return cls(t[1], t[0])
@classmethod
def from_osrm_tuple(cls, t: list[float]) -> Coordinate:
"""OSRM uses [lon, lat] order."""
return cls(t[1], t[0])
def as_dict(self) -> dict[str, float]:
return {'latitude': self.latitude, 'longitude': self.longitude}
def __str__(self) -> str:
return f'Coordinate({self.latitude}, {self.longitude})'
def __repr__(self) -> str:
return self.__str__()
def __eq__(self, other) -> bool:
if not isinstance(other, Coordinate):
return False
return self.latitude == other.latitude and self.longitude == other.longitude
def __hash__(self) -> int:
return hash((self.latitude, self.longitude))
def __sub__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude - other.latitude, self.longitude - other.longitude)
def __add__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude + other.latitude, self.longitude + other.longitude)
def __mul__(self, c: float) -> Coordinate:
return Coordinate(self.latitude * c, self.longitude * c)
def dot(self, other: Coordinate) -> float:
return self.latitude * other.latitude + self.longitude * other.longitude
def distance_to(self, other: Coordinate) -> float:
"""Haversine distance in meters."""
dlat = math.radians(other.latitude - self.latitude)
dlon = math.radians(other.longitude - self.longitude)
haversine_dlat = math.sin(dlat / 2.0)
haversine_dlat *= haversine_dlat
haversine_dlon = math.sin(dlon / 2.0)
haversine_dlon *= haversine_dlon
y = haversine_dlat \
+ math.cos(math.radians(self.latitude)) \
* math.cos(math.radians(other.latitude)) \
* haversine_dlon
x = 2 * math.asin(math.sqrt(y))
return x * EARTH_MEAN_RADIUS
def bearing_to(self, other: Coordinate) -> float:
"""Bearing to other coordinate in degrees (0-360)."""
lat1, lat2 = math.radians(self.latitude), math.radians(other.latitude)
dlon = math.radians(other.longitude - self.longitude)
x = math.sin(dlon) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
bearing = math.degrees(math.atan2(x, y))
return (bearing + 360) % 360
def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate) -> float:
"""Minimum distance from point p to line segment ab."""
if a.distance_to(b) < 0.01:
return a.distance_to(p)
ap = p - a
ab = b - a
t = max(0.0, min(1.0, ap.dot(ab) / ab.dot(ab)))
projection = a + ab * t
return projection.distance_to(p)
def project_onto_segment(a: Coordinate, b: Coordinate, p: Coordinate) -> tuple[Coordinate, float, float]:
"""Project point p onto line segment ab (snap to road).
Returns:
(projected_point, t, distance) where:
- projected_point: the closest point on segment ab to p
- t: parameter 0-1 indicating position along segment (0=a, 1=b)
- distance: distance from p to the projected point
"""
seg_len = a.distance_to(b)
if seg_len < 0.01:
return a, 0.0, a.distance_to(p)
ap = p - a
ab = b - a
t = max(0.0, min(1.0, ap.dot(ab) / ab.dot(ab)))
projection = a + ab * t
return projection, t, projection.distance_to(p)
def distance_along_geometry(geometry: list[Coordinate], pos: Coordinate) -> float:
"""Calculate distance traveled along geometry from start to closest point to pos."""
if len(geometry) <= 2:
return geometry[0].distance_to(pos)
total_distance = 0.0
total_distance_closest = 0.0
closest_distance = 1e9
for i in range(len(geometry) - 1):
d = minimum_distance(geometry[i], geometry[i + 1], pos)
if d < closest_distance:
closest_distance = d
total_distance_closest = total_distance + geometry[i].distance_to(pos)
total_distance += geometry[i].distance_to(geometry[i + 1])
return total_distance_closest
def normalize_angle(angle: float) -> float:
"""Normalize angle to -180 to 180 degrees."""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def calculate_turn_angle(
current_geometry: list[Coordinate],
next_geometry: list[Coordinate],
samples: int = 3
) -> float:
"""
Calculate turn angle between two road segments.
Uses the bearing of the end of current geometry vs the bearing
of the start of next geometry to determine turn angle.
Args:
current_geometry: Coordinates of current road segment (before turn)
next_geometry: Coordinates of next road segment (after turn)
samples: Number of points to use for bearing calculation (for stability)
Returns:
Turn angle in degrees. Positive = left turn, negative = right turn.
Example: 90 = 90° left turn, -90 = 90° right turn
"""
if len(current_geometry) < 2 or len(next_geometry) < 2:
return 0.0
# Get bearing of current road (last segment)
# Use last few points for stability
start_idx = max(0, len(current_geometry) - samples)
current_bearing = current_geometry[start_idx].bearing_to(current_geometry[-1])
# Get bearing of next road (first segment)
# Use first few points for stability
end_idx = min(samples, len(next_geometry) - 1)
next_bearing = next_geometry[0].bearing_to(next_geometry[end_idx])
# Calculate turn angle (positive = left, negative = right)
# This matches openpilot convention where left is positive curvature
angle = normalize_angle(current_bearing - next_bearing)
return angle
def coordinate_from_param(key: str, params: Params = None) -> Coordinate | None:
"""Read coordinate from params.
Handles both JSON type params (returns dict) and STRING type params (returns string).
"""
if params is None:
params = Params()
try:
value = params.get(key)
if value is None:
return None
# JSON type params return dict directly, STRING type needs json.loads()
if isinstance(value, str):
pos = json.loads(value)
else:
pos = value
if 'latitude' not in pos or 'longitude' not in pos:
return None
return Coordinate(pos['latitude'], pos['longitude'])
except (json.JSONDecodeError, KeyError, TypeError):
return None
def find_closest_point_on_route(pos: Coordinate, route_coords: list[Coordinate]) -> tuple[int, float]:
"""Find closest point index and distance on route."""
if not route_coords:
return 0, float('inf')
min_dist = float('inf')
closest_idx = 0
for i in range(len(route_coords) - 1):
# Check distance to segment, not just point
d = minimum_distance(route_coords[i], route_coords[i + 1], pos)
if d < min_dist:
min_dist = d
closest_idx = i
return closest_idx, min_dist
def calculate_remaining_distance(route_coords: list[Coordinate], from_idx: int) -> float:
"""Calculate remaining distance along route from index."""
if from_idx >= len(route_coords) - 1:
return 0.0
total = 0.0
for i in range(from_idx, len(route_coords) - 1):
total += route_coords[i].distance_to(route_coords[i + 1])
return total
# --- Instruction Parsing ---
def string_to_direction(direction: str) -> str:
"""Convert direction string to standard format."""
for d in DIRECTIONS:
if d in direction:
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
return 'slight' + d.capitalize()
return d
return 'none'
def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float:
"""Convert speed limit dict to m/s."""
unit = cast(str, maxspeed['unit'])
speed = cast(float, maxspeed['speed'])
return SPEED_CONVERSIONS.get(unit, 1.0) * speed
def field_valid(dat: dict, field: str) -> bool:
"""Check if field exists and is not None."""
return field in dat and dat[field] is not None
def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> dict[str, Any] | None:
"""Parse Mapbox/OSRM banner instructions."""
if not banners or not len(banners):
return None
instruction = {}
# A segment can contain multiple banners, find one that we need to show now
current_banner = banners[0]
for banner in banners:
if distance_to_maneuver < banner.get('distanceAlongGeometry', 0):
current_banner = banner
# Only show banner when close enough to maneuver
instruction['showFull'] = distance_to_maneuver < current_banner.get('distanceAlongGeometry', 0)
# Primary
p = current_banner.get('primary', {})
if field_valid(p, 'text'):
instruction['maneuverPrimaryText'] = p['text']
if field_valid(p, 'type'):
instruction['maneuverType'] = p['type']
if field_valid(p, 'modifier'):
instruction['maneuverModifier'] = p['modifier']
# Secondary
if field_valid(current_banner, 'secondary'):
instruction['maneuverSecondaryText'] = current_banner['secondary']['text']
# Lane lines
if field_valid(current_banner, 'sub'):
lanes = []
for component in current_banner['sub'].get('components', []):
if component.get('type') != 'lane':
continue
lane = {
'active': component.get('active', False),
'directions': [string_to_direction(d) for d in component.get('directions', [])],
}
if field_valid(component, 'active_direction'):
lane['activeDirection'] = string_to_direction(component['active_direction'])
lanes.append(lane)
instruction['lanes'] = lanes
return instruction
def parse_osrm_step(step: dict) -> dict[str, Any]:
"""Parse OSRM route step into instruction format."""
maneuver = step.get('maneuver', {})
instruction = {
'distance': step.get('distance', 0),
'duration': step.get('duration', 0),
'name': step.get('name', ''),
'maneuverType': maneuver.get('type', ''),
'maneuverModifier': maneuver.get('modifier', ''),
'location': maneuver.get('location', []), # [lon, lat]
}
return instruction
def classify_maneuver(maneuver_type: str, maneuver_modifier: str) -> str:
"""
Classify OSRM maneuver as 'turn' or 'laneChange'.
Highway exits/forks use laneChange desires (smoother).
Intersection turns use turn desires (sharper).
OSRM maneuver types:
- turn: regular intersection turn
- fork: highway split/junction
- off ramp: highway exit
- on ramp: highway entrance
- merge: merging lanes
- roundabout turn: roundabout exit
- exit roundabout: leaving roundabout
- continue: straight (no maneuver)
- depart/arrive: start/end
Returns:
'turn' for intersection turns
'laneChange' for highway exits/forks
'none' for straight/no maneuver
"""
maneuver_type = maneuver_type.lower()
maneuver_modifier = maneuver_modifier.lower()
# Highway exits and forks -> lane change desire
LANE_CHANGE_TYPES = {
'fork', # highway fork/split
'off ramp', # highway exit
'on ramp', # highway entrance
'merge', # merging
'exit rotary', # leaving rotary
'exit roundabout',# leaving roundabout
}
# Intersection turns -> turn desire
TURN_TYPES = {
'turn', # regular turn
'end of road', # forced turn at end of road
'rotary', # entering rotary
'roundabout', # entering roundabout
'roundabout turn',# turn within roundabout
}
# No maneuver
NO_MANEUVER_TYPES = {
'continue',
'depart',
'arrive',
'new name',
'notification',
}
if maneuver_type in LANE_CHANGE_TYPES:
return 'laneChange'
if maneuver_type in TURN_TYPES:
# For turns, check modifier - slight turns at highway speeds might be lane changes
if 'slight' in maneuver_modifier:
# Slight turns could be either - default to turn but could be lane change
# CarrotPilot uses additional context like road speed limit
return 'turn'
return 'turn'
if maneuver_type in NO_MANEUVER_TYPES:
return 'none'
# Unknown type - default to turn
return 'turn'
def get_turn_direction(maneuver_modifier: str) -> str:
"""
Get turn direction from OSRM maneuver modifier.
Returns:
'left', 'right', or 'none'
"""
modifier = maneuver_modifier.lower()
if 'left' in modifier:
return 'left'
if 'right' in modifier:
return 'right'
return 'none'
# --- Curvature Computation ---
def compute_path_curvature(pos: Coordinate, bearing: float, route_coords: list[Coordinate],
closest_idx: int, v_ego: float, lookahead_time: float = 2.5) -> float:
"""
Compute desired curvature from route geometry using pure pursuit.
Args:
pos: Current position
bearing: Current heading in degrees
route_coords: List of route coordinates
closest_idx: Index of closest point on route
v_ego: Current vehicle speed m/s
lookahead_time: How far ahead to look in seconds
Returns:
Desired curvature in 1/m (positive = left turn, negative = right turn)
"""
if not route_coords or closest_idx >= len(route_coords) - 1:
return 0.0
# Calculate lookahead distance (min 30m, based on speed)
lookahead_dist = max(v_ego * lookahead_time, 30.0)
# Find lookahead point along route
dist_traveled = 0.0
lookahead_idx = closest_idx
for i in range(closest_idx, len(route_coords) - 1):
segment_dist = route_coords[i].distance_to(route_coords[i + 1])
if dist_traveled + segment_dist >= lookahead_dist:
# Interpolate within segment
remaining = lookahead_dist - dist_traveled
ratio = remaining / segment_dist if segment_dist > 0 else 0
lookahead_idx = i
# Could interpolate here, but using next point is simpler
if ratio > 0.5:
lookahead_idx = i + 1
break
dist_traveled += segment_dist
lookahead_idx = i + 1
if lookahead_idx >= len(route_coords):
lookahead_idx = len(route_coords) - 1
lookahead_point = route_coords[lookahead_idx]
# Calculate desired heading to lookahead point
desired_bearing = pos.bearing_to(lookahead_point)
# Calculate heading error (normalized to -180 to 180)
heading_error = desired_bearing - bearing
if heading_error > 180:
heading_error -= 360
elif heading_error < -180:
heading_error += 360
# Convert heading error to yaw (radians)
yaw_error = math.radians(heading_error)
# Distance to lookahead point
dist_to_lookahead = pos.distance_to(lookahead_point)
if dist_to_lookahead < 1.0:
return 0.0
# Pure pursuit curvature: 2 * sin(yaw_error) / lookahead_distance
# Note: Negative because heading_error > 0 means target is to the RIGHT (clockwise),
# and right turn should produce negative curvature
curvature = -2.0 * math.sin(yaw_error) / dist_to_lookahead
# Clamp to reasonable values (max ~7m radius turn)
MAX_CURVATURE = 0.15
return max(-MAX_CURVATURE, min(MAX_CURVATURE, curvature))
def smooth_curvature(new_curv: float, prev_curv: float, alpha: float = 0.3) -> float:
"""Exponential smoothing for curvature."""
return alpha * new_curv + (1 - alpha) * prev_curv
def curvature_to_radius(curvature: float) -> float:
"""Convert curvature to turn radius in meters."""
if abs(curvature) < 0.001:
return float('inf')
return 1.0 / abs(curvature)
def compute_turn_angle_at_index(route_coords: list[Coordinate], turn_idx: int,
sample_dist: float = 20.0) -> float:
"""
Compute turn angle at a specific point on the route.
Uses points sample_dist meters before and after for stability.
Returns angle in degrees (positive = left, negative = right).
"""
if turn_idx < 1 or turn_idx >= len(route_coords) - 1:
return 0.0
# Find points ~sample_dist before and after the turn for stable bearing
before_idx = turn_idx
after_idx = turn_idx
# Walk backwards to find before point
dist = 0.0
for i in range(turn_idx, 0, -1):
dist += route_coords[i].distance_to(route_coords[i - 1])
if dist >= sample_dist:
before_idx = i - 1
break
else:
before_idx = 0
# Walk forwards to find after point
dist = 0.0
for i in range(turn_idx, len(route_coords) - 1):
dist += route_coords[i].distance_to(route_coords[i + 1])
if dist >= sample_dist:
after_idx = i + 1
break
else:
after_idx = len(route_coords) - 1
if before_idx == turn_idx or after_idx == turn_idx:
return 0.0
# Calculate bearings using the sampled points
p1 = route_coords[before_idx]
p2 = route_coords[turn_idx]
p3 = route_coords[after_idx]
bearing1 = p1.bearing_to(p2)
bearing2 = p2.bearing_to(p3)
# Angle difference (positive = left, negative = right)
angle = bearing1 - bearing2
# Normalize to -180 to 180
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def compute_turn_curvature_at_index(route_coords: list[Coordinate], turn_idx: int,
sample_dist: float = 15.0) -> float:
"""
Compute curvature at turn point using three-point circle fitting.
Returns curvature in 1/m (positive = left, negative = right).
"""
if turn_idx < 1 or turn_idx >= len(route_coords) - 1:
return 0.0
# Find points sample_dist before and after
before_idx = turn_idx
after_idx = turn_idx
dist = 0.0
for i in range(turn_idx, 0, -1):
dist += route_coords[i].distance_to(route_coords[i - 1])
if dist >= sample_dist:
before_idx = i - 1
break
dist = 0.0
for i in range(turn_idx, len(route_coords) - 1):
dist += route_coords[i].distance_to(route_coords[i + 1])
if dist >= sample_dist:
after_idx = i + 1
break
if before_idx == turn_idx or after_idx == turn_idx:
return 0.0
# Three points for circle fitting
p1 = route_coords[before_idx]
p2 = route_coords[turn_idx]
p3 = route_coords[after_idx]
# Convert to local meters (approximate)
lat_center = p2.latitude
lon_scale = math.cos(math.radians(lat_center))
m_per_deg = 111319.5 # meters per degree latitude
x1 = (p1.longitude - p2.longitude) * lon_scale * m_per_deg
y1 = (p1.latitude - p2.latitude) * m_per_deg
x2 = 0.0
y2 = 0.0
x3 = (p3.longitude - p2.longitude) * lon_scale * m_per_deg
y3 = (p3.latitude - p2.latitude) * m_per_deg
# Calculate curvature using cross product / (product of distances)
d12 = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
d13 = math.sqrt((x3 - x1)**2 + (y3 - y1)**2)
d23 = math.sqrt((x3 - x2)**2 + (y3 - y2)**2)
if d12 < 0.1 or d13 < 0.1 or d23 < 0.1:
return 0.0
# Cross product (p2-p1) x (p3-p1) - z component only
cross = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)
curvature = 2.0 * cross / (d12 * d13 * d23)
# Clamp to reasonable values
MAX_CURVATURE = 0.2 # ~5m radius
return max(-MAX_CURVATURE, min(MAX_CURVATURE, curvature))
+47
View File
@@ -0,0 +1,47 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dragonpilot Map-Aware Assist Library
Core modules:
- maa_desire: Simple turn desire logic (blinker confirmation, lane change blocking, RHD/LHD)
- model_helper: Helper class for modeld integration
- longitudinal_helper: Planner integration for speed/accel limiting
"""
from dragonpilot.dashy.maa.lib.maa_desire import (
should_block_lane_change,
get_turn_desire,
is_crossing_turn,
get_turn_trigger_distance,
)
from dragonpilot.dashy.maa.lib.model_helper import ModelHelper
from dragonpilot.dashy.maa.lib.longitudinal_helper import LongitudinalHelper
__all__ = [
'should_block_lane_change',
'get_turn_desire',
'is_crossing_turn',
'get_turn_trigger_distance',
'ModelHelper',
'LongitudinalHelper',
]
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Acceleration Limiter
Physics-based acceleration limiting for turns.
Algorithm adapted from CarrotPilot (https://github.com/ajouatom/openpilot)
Credit: carrotpilot team for the physics-based lateral acceleration approach.
The key insight is that total acceleration is limited by tire grip:
a_total² = a_x² + a_y² a_max²
Where:
a_x = longitudinal acceleration (throttle/brake)
a_y = lateral acceleration (from turning) = × curvature
This means during turns, we must reduce longitudinal acceleration
to stay within the grip circle.
"""
import math
from dataclasses import dataclass
from typing import Tuple, Optional
@dataclass
class AccelLimits:
"""Acceleration limits."""
min_accel: float # m/s² (negative = braking)
max_accel: float # m/s² (positive = acceleration)
@dataclass
class AccelLimiterState:
"""Current state of acceleration limiter."""
v_ego: float = 0.0
curvature: float = 0.0
lateral_accel: float = 0.0
available_long_accel: float = 0.0
is_limiting: bool = False
class AccelLimiter:
"""
Physics-based acceleration limiter for turns.
Uses the friction circle concept: total acceleration magnitude
is limited by tire grip. During turns, lateral acceleration
consumes part of this budget, leaving less for longitudinal
acceleration.
Adapted from CarrotPilot's limit_accel_in_turns function.
"""
# Default lateral acceleration limit (m/s²)
# Comfortable limit is ~2-3 m/s², sporty is ~4 m/s²
DEFAULT_LAT_ACCEL_MAX = 2.5
# Lookup table for max total acceleration vs speed
# Higher speeds = lower max lateral accel for comfort
A_TOTAL_MAX_BP = [0., 10., 20., 30., 40.] # m/s
A_TOTAL_MAX_V = [3.0, 2.8, 2.5, 2.2, 2.0] # m/s²
def __init__(
self,
lat_accel_max: float = DEFAULT_LAT_ACCEL_MAX,
comfort_mode: bool = True
):
"""
Initialize acceleration limiter.
Args:
lat_accel_max: Maximum allowed lateral acceleration (m/)
comfort_mode: If True, use speed-dependent limits
"""
self.lat_accel_max = lat_accel_max
self.comfort_mode = comfort_mode
self.state = AccelLimiterState()
def get_max_total_accel(self, v_ego: float) -> float:
"""
Get maximum total acceleration for current speed.
In comfort mode, reduces limit at higher speeds.
"""
if not self.comfort_mode:
return self.lat_accel_max
# Interpolate from lookup table
import numpy as np
return np.interp(v_ego, self.A_TOTAL_MAX_BP, self.A_TOTAL_MAX_V)
def compute_lateral_accel(self, v_ego: float, curvature: float) -> float:
"""
Compute lateral acceleration from speed and curvature.
a_y = × κ
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
Returns:
Lateral acceleration in m/
"""
return v_ego * v_ego * abs(curvature)
def compute_available_long_accel(
self,
v_ego: float,
curvature: float,
a_max: Optional[float] = None
) -> float:
"""
Compute available longitudinal acceleration given current turn.
Uses friction circle: a_x² + a_y² a_max²
Solving for a_x: a_x = sqrt(a_max² - a_y²)
Adapted from CarrotPilot's limit_accel_in_turns.
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
a_max: Override for max total acceleration
Returns:
Maximum available longitudinal acceleration in m/
"""
if a_max is None:
a_max = self.get_max_total_accel(v_ego)
# Compute lateral acceleration
a_y = self.compute_lateral_accel(v_ego, curvature)
a_y_abs = abs(a_y)
# Update state
self.state.v_ego = v_ego
self.state.curvature = curvature
self.state.lateral_accel = a_y
# Check if lateral accel exceeds limit
if a_y_abs >= a_max:
# Already at or over limit - no longitudinal accel available
self.state.available_long_accel = 0.0
self.state.is_limiting = True
return 0.0
# Compute remaining budget for longitudinal acceleration
a_x_available = math.sqrt(a_max * a_max - a_y_abs * a_y_abs)
self.state.available_long_accel = a_x_available
self.state.is_limiting = a_x_available < a_max * 0.9 # Limiting if < 90% available
return a_x_available
def limit_accel(
self,
v_ego: float,
curvature: float,
accel_limits: AccelLimits,
a_max: Optional[float] = None
) -> AccelLimits:
"""
Apply turn-based acceleration limiting.
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
accel_limits: Current acceleration limits
a_max: Override for max total acceleration
Returns:
New AccelLimits with turn limiting applied
"""
a_x_available = self.compute_available_long_accel(v_ego, curvature, a_max)
# Clamp max acceleration to available budget
new_max = min(accel_limits.max_accel, a_x_available)
# Don't limit braking as much - we may need to slow down
# But still apply some limit for comfort
new_min = max(accel_limits.min_accel, -a_x_available * 1.5)
return AccelLimits(
min_accel=new_min,
max_accel=new_max
)
def limit_accel_tuple(
self,
v_ego: float,
curvature: float,
accel_limits: Tuple[float, float],
a_max: Optional[float] = None
) -> Tuple[float, float]:
"""
Apply turn-based acceleration limiting (tuple interface).
For compatibility with existing planner code.
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
accel_limits: (min_accel, max_accel) tuple
a_max: Override for max total acceleration
Returns:
(min_accel, max_accel) tuple with turn limiting applied
"""
limits = self.limit_accel(
v_ego,
curvature,
AccelLimits(accel_limits[0], accel_limits[1]),
a_max
)
return (limits.min_accel, limits.max_accel)
@@ -0,0 +1,360 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Map-Aware Assist Longitudinal Helper
Provides navigation-based speed and acceleration limiting for the longitudinal planner.
This module keeps all nav-related planner logic isolated from the core planner code.
Features:
- Turn speed limiting based on maaControl cereal message
- Physics-based acceleration limiting using friction circle
- Smooth slowdown/resume transitions
- Driver acknowledgment (blinker) required to activate speed limiting
Adapted from CarrotPilot (https://github.com/ajouatom/openpilot)
Credit: carrotpilot team for curvature-based speed and physics-based accel limiting.
Usage in longitudinal_planner.py:
from dragonpilot.dashy.maa.lib.longitudinal_helper import LongitudinalHelper
# Add 'maaControl' to SubMaster
# In update:
maa = sm['maaControl']
if sm.valid['maaControl'] and maa.turnValid and maa.speedLimitActive:
# Only apply speed limit when driver acknowledged (blinker on)
turn_speed = maa.turnSpeedLimit
turn_distance = maa.turnDistance
else:
turn_speed, turn_distance = None, None
v_cruise = self.maa_helper.apply_nav_speed_limit(v_cruise, turn_speed, turn_distance)
"""
from dataclasses import dataclass
from typing import Optional, List
# Import nav modules
try:
from dragonpilot.dashy.maa.lib.accel_limiter import AccelLimiter
ACCEL_LIMITER_AVAILABLE = True
except ImportError:
ACCEL_LIMITER_AVAILABLE = False
@dataclass
class NavPlannerState:
"""Current state of nav planner integration."""
nav_limited_speed: Optional[float] = None
is_limiting_speed: bool = False
is_limiting_accel: bool = False
@dataclass
class VirtualLead:
"""Virtual lead car for turn deceleration."""
status: bool = False
dRel: float = 100.0 # distance to virtual lead
vLead: float = 0.0 # speed of virtual lead
aLeadK: float = 0.0 # acceleration
aLeadTau: float = 0.3
class RadarStateWrapper:
"""
Wrapper to inject virtual lead into radarState for MPC.
This allows the MPC to "see" a fake slow car at the turn point,
triggering natural deceleration using the well-tuned lead following logic.
"""
def __init__(self, radar_state, virtual_lead=None):
self._radar_state = radar_state
self._virtual_lead = virtual_lead
@property
def leadOne(self):
real_lead = self._radar_state.leadOne
# If no virtual lead, use real lead
if self._virtual_lead is None or not self._virtual_lead.status:
return real_lead
# If real lead exists and is closer, use real lead
if real_lead.status and real_lead.dRel < self._virtual_lead.dRel:
return real_lead
# Use virtual lead (turn point)
return self._virtual_lead
@property
def leadTwo(self):
return self._radar_state.leadTwo
def __getattr__(self, name):
return getattr(self._radar_state, name)
class LongitudinalHelper:
"""
Navigation helper for longitudinal planner.
Provides turn speed limiting and physics-based acceleration limiting
based on navigation data. Keeps all nav logic isolated from core planner.
Design rationale (based on automotive research):
- Stopping distance at 50 km/h is ~48m (25m braking + 15m reaction)
- Intersection approach speeds typically 30-50 km/h
- Comfortable deceleration: 1.5-2.0 m/ for normal driving
"""
# Configuration - Speed limiting
# Physics-based: calculate braking distance from speed difference
# More natural driving: maintain speed, brake closer to turn
SLOWDOWN_END_DIST = 10.0 # meters - be at turn speed by this distance (buffer before turn)
SLOWDOWN_BUFFER = 15.0 # meters - extra buffer added to braking distance
# Physics-based accel limiting
ACCEL_LIMIT_ENABLED = True
LAT_ACCEL_MAX = 2.5 # m/s² - max comfortable lateral acceleration
# Comfortable deceleration target (m/s²)
# Research shows 1.5-2.0 m/s² is comfortable for passengers
# Using 2.0 for more natural, later braking
COMFORT_DECEL = 2.0
def __init__(self):
"""Initialize nav planner helper."""
self.state = NavPlannerState()
# Physics-based acceleration limiter
if ACCEL_LIMITER_AVAILABLE and self.ACCEL_LIMIT_ENABLED:
self.accel_limiter = AccelLimiter(
lat_accel_max=self.LAT_ACCEL_MAX,
comfort_mode=True
)
else:
self.accel_limiter = None
def apply_nav_speed_limit(
self,
v_cruise: float,
turn_speed: Optional[float] = None,
distance: Optional[float] = None
) -> float:
"""
Apply navigation-based speed limiting with physics-based braking.
Uses kinematic equation: d = ( - v_target²) / (2 * decel)
Starts braking at calculated distance + buffer for natural driving feel.
Args:
v_cruise: Current cruise speed in m/s
turn_speed: Turn speed limit from maaControl (None if invalid)
distance: Distance to turn from maaControl (None if invalid)
Returns:
Limited cruise speed
"""
if turn_speed is None or distance is None:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
return v_cruise
# No need to slow if already at or below turn speed
if v_cruise <= turn_speed:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
return v_cruise
# Calculate required braking distance using kinematics
# d = (v² - v_target²) / (2 * decel)
speed_diff_sq = v_cruise ** 2 - turn_speed ** 2
braking_distance = speed_diff_sq / (2 * self.COMFORT_DECEL)
# Start braking at: braking_distance + buffer + end_distance
slowdown_start = braking_distance + self.SLOWDOWN_BUFFER + self.SLOWDOWN_END_DIST
# Outside slowdown zone - no limit
if distance > slowdown_start:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
return v_cruise
# In slowdown zone - calculate target speed at this distance
# v² = v_target² + 2 * decel * (distance - end_dist)
if distance > self.SLOWDOWN_END_DIST:
remaining = distance - self.SLOWDOWN_END_DIST
# Target speed that allows comfortable braking to turn_speed
target_speed_sq = turn_speed ** 2 + 2 * self.COMFORT_DECEL * remaining
limited_speed = min(v_cruise, target_speed_sq ** 0.5)
else:
# Very close to turn - calculate achievable speed with comfort decel
# This handles late blinker: never brake harder than COMFORT_DECEL
# If we can't reach turn_speed in time, accept entering turn faster
achievable_speed_sq = turn_speed ** 2 + 2 * self.COMFORT_DECEL * max(0, distance)
achievable_speed = achievable_speed_sq ** 0.5
# Never target lower than achievable (prevents harsh braking)
limited_speed = min(v_cruise, max(turn_speed, achievable_speed))
self.state.nav_limited_speed = limited_speed
self.state.is_limiting_speed = True
return limited_speed
# Minimum distance for virtual lead - below this, don't use virtual lead
# to avoid MPC braking to stop
VIRTUAL_LEAD_MIN_DIST = 15.0 # meters
def get_virtual_lead(
self,
v_ego: float,
turn_speed: Optional[float] = None,
distance: Optional[float] = None
) -> Optional[VirtualLead]:
"""
Create a virtual lead car at the turn point for natural deceleration.
The virtual lead "drives" at turn_speed, positioned at the turn point.
This makes the MPC decelerate naturally as if following a slow car.
Args:
v_ego: Current vehicle speed in m/s
turn_speed: Turn speed limit from maaControl (None if invalid)
distance: Distance to turn from maaControl (None if invalid)
Returns:
VirtualLead object if turn is approaching, None otherwise
"""
if turn_speed is None or distance is None:
return None
# Only create virtual lead when we need to slow down
if v_ego <= turn_speed:
return None
# Don't use virtual lead when very close to turn - avoids brake-to-stop
# At this point, we should already be at turn speed from earlier braking
if distance < self.VIRTUAL_LEAD_MIN_DIST:
return None
# Calculate when to start showing virtual lead
# Use same physics: braking distance + buffer
speed_diff_sq = v_ego ** 2 - turn_speed ** 2
braking_distance = speed_diff_sq / (2 * self.COMFORT_DECEL)
activation_distance = braking_distance + self.SLOWDOWN_BUFFER + self.SLOWDOWN_END_DIST
if distance > activation_distance:
return None
# Create virtual lead at turn point, moving at turn speed
# The MPC will naturally decelerate to follow it
return VirtualLead(
status=True,
dRel=distance, # distance to virtual lead
vLead=turn_speed, # virtual lead moves at turn speed
aLeadK=0.0, # no acceleration (constant speed)
aLeadTau=0.3 # response time constant
)
def apply_nav_accel_limit(
self,
v_ego: float,
curvature: float,
accel_clip: List[float]
) -> List[float]:
"""
Apply physics-based acceleration limiting for turns.
Uses friction circle: a_x² + a_y² a_max²
Args:
v_ego: Current vehicle speed in m/s
curvature: Current road curvature (from nav or model)
accel_clip: Current [min_accel, max_accel] limits
Returns:
Updated [min_accel, max_accel] with turn limiting applied
"""
if self.accel_limiter is None:
return accel_clip
if abs(curvature) < 0.001: # Essentially straight
self.state.is_limiting_accel = False
return accel_clip
limited = list(self.accel_limiter.limit_accel_tuple(
v_ego, curvature, tuple(accel_clip)
))
self.state.is_limiting_accel = self.accel_limiter.state.is_limiting
return limited
# Staleness threshold for maaControl message (nanoseconds)
STALE_THRESHOLD_NS = 5e8 # 0.5 seconds
def process(
self,
sm,
v_ego: float,
v_cruise: float,
accel_clip: List[float]
) -> tuple:
"""
Process maaControl and return updated planner values.
Encapsulates all MAA logic:
- Validity and staleness checking
- Speed limiting (when driver acknowledged via blinker)
- Virtual lead creation for natural deceleration
- Curvature-based acceleration limiting
Args:
sm: SubMaster with 'maaControl' and 'carState'
v_ego: Current vehicle speed in m/s
v_cruise: Current cruise speed in m/s
accel_clip: Current [min_accel, max_accel] limits
Returns:
tuple: (v_cruise, accel_clip, virtual_lead)
"""
virtual_lead = None
maa = sm['maaControl']
# Check valid and not stale
maa_valid = sm.valid['maaControl'] and maa.turnValid
if maa_valid and (sm.logMonoTime['carState'] - sm.logMonoTime['maaControl']) > self.STALE_THRESHOLD_NS:
maa_valid = False
if not maa_valid:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
self.state.is_limiting_accel = False
return v_cruise, accel_clip, virtual_lead
# Speed limiting only when driver acknowledged (blinker on)
# Without blinker: informational only, no speed reduction
if maa.speedLimitActive:
virtual_lead = self.get_virtual_lead(v_ego, maa.turnSpeedLimit, maa.turnDistance)
v_cruise = self.apply_nav_speed_limit(v_cruise, maa.turnSpeedLimit, maa.turnDistance)
# Curvature-based acceleration limiting (always active when valid)
if maa.curvatureValid and abs(maa.curvature) > 0.001:
accel_clip = self.apply_nav_accel_limit(v_ego, maa.curvature, accel_clip)
return v_cruise, accel_clip, virtual_lead
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MAA Desire Helper
Turn execution flow:
1. APPROACHING (200m): Start dead reckoning, show turn suggestion
2. Driver turns on blinker: acknowledged
3. At 100m with blinker: COMMIT - block lane change, slow down, send desire
4. EXECUTING (30m): Track heading change
5. COMPLETE: When heading change expected turn angle
Turn desire is sent when:
- desireActive is true (from maa_controld)
- This requires: driver acknowledged (blinker) + committed (<100m) + EXECUTING state
Without blinker:
- System shows turn info but doesn't intervene
- No speed reduction, no desire sent
- Driver maintains full control
"""
from cereal import log, custom
TurnDirection = custom.MaaControl.TurnDirection
ManeuverType = custom.MaaControl.ManeuverType
TurnState = {
'NONE': 0,
'APPROACHING': 1,
'EXECUTING': 2,
'COMPLETE': 3,
'MISSED': 4,
}
# Configuration
MAX_TURN_SPEED = 50.0 / 3.6 # m/s (50 km/h) - no turn assist above this
SHARP_TURN_ANGLE = 45.0 # degrees - above: turnLeft/Right, below: laneChangeLeft/Right
def should_block_lane_change(maa_control, v_ego: float) -> bool:
"""
Check if lane change should be blocked due to approaching turn.
Uses blockLaneChange from maa_controld which is set when:
- Driver acknowledged (blinker on matching direction)
- Within commit distance (100m)
"""
if maa_control is None:
return False
if v_ego > MAX_TURN_SPEED:
return False
# Use the pre-computed blockLaneChange from maa_controld
return maa_control.blockLaneChange
def get_turn_desire(maa_control, carstate, is_rhd: bool = False) -> log.Desire:
"""
Get turn desire based on maaControl.
desireActive from maa_controld is true when:
- Driver acknowledged (blinker matches turn direction)
- Committed (<100m) OR in EXECUTING state
This function adds additional checks:
- Speed < 50 km/h
- maneuverType == turn
Args:
maa_control: MaaControl message
carstate: CarState message
is_rhd: True if right-hand drive (UK/Japan), False for left-hand drive (US/Taiwan)
Returns:
- turnLeft/Right if angle >= 45°
- laneChangeLeft/Right if angle < 45°
- none if conditions not met
"""
if maa_control is None or not maa_control.turnValid:
return log.Desire.none
if maa_control.maneuverType != ManeuverType.turn:
return log.Desire.none
if carstate.vEgo > MAX_TURN_SPEED:
return log.Desire.none
# desireActive encapsulates: acknowledged + (committed OR executing)
if not maa_control.desireActive:
return log.Desire.none
# Sharp turn (>= 45°) uses turnLeft/Right, gentle uses laneChange
is_sharp = abs(maa_control.turnAngle) >= SHARP_TURN_ANGLE
if maa_control.turnDirection == TurnDirection.left:
return log.Desire.turnLeft if is_sharp else log.Desire.laneChangeLeft
elif maa_control.turnDirection == TurnDirection.right:
return log.Desire.turnRight if is_sharp else log.Desire.laneChangeRight
return log.Desire.none
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Model Helper for modeld integration.
Provides turn desire logic for modeld.py.
"""
from cereal import log
from dragonpilot.dashy.maa.lib.maa_desire import (
should_block_lane_change,
get_turn_desire,
)
class ModelHelper:
"""
Helper class for MAA integration with modeld.
Usage in modeld.py:
from dragonpilot.dashy.maa.lib import ModelHelper
model_helper = ModelHelper()
# In the loop:
is_rhd = sm["driverMonitoringState"].isRHD
desire = model_helper.get_desire(sm, lane_change_desire, is_rhd)
"""
def __init__(self):
self.active = False
self.last_desire = log.Desire.none
def get_desire(self, sm, lane_change_desire: int, is_rhd: bool = False) -> int:
"""
Get combined desire from MAA turn logic and lane change.
Priority:
1. Active lane change (driver initiated) - don't interrupt
2. MAA turn desire (navigation turn)
3. Lane change desire (pre-lane-change, keep, etc.)
Args:
sm: SubMaster with maaControl and carState
lane_change_desire: Desire from DesireHelper (lane change logic)
is_rhd: True if right-hand drive
Returns:
Final desire value
"""
# Check if MAA data is available
if not sm.valid.get('maaControl', False) or not sm.valid.get('carState', False):
return lane_change_desire
# Check for stale maaControl (wait for 0.5s)
if (sm.logMonoTime['carState'] - sm.logMonoTime['maaControl']) > 5e8:
return lane_change_desire
maa_control = sm['maaControl']
carstate = sm['carState']
# Don't interrupt active lane change
if lane_change_desire in (log.Desire.laneChangeLeft, log.Desire.laneChangeRight):
self.active = False
return lane_change_desire
# Check if lane change should be blocked due to approaching turn
if should_block_lane_change(maa_control, carstate.vEgo):
# Block lane change desires, but allow none/keep
if lane_change_desire in (log.Desire.laneChangeLeft, log.Desire.laneChangeRight):
lane_change_desire = log.Desire.none
# Get MAA turn desire
maa_desire = get_turn_desire(maa_control, carstate, is_rhd)
# MAA turn desire takes priority over none/keep
if maa_desire != log.Desire.none:
self.active = True
self.last_desire = maa_desire
return maa_desire
# If MAA was active but now returns none, we completed the turn
if self.active and maa_desire == log.Desire.none:
self.active = False
return lane_change_desire
def update(self, modelv2, desire_state):
"""
Update helper with model output (for future use).
Can be used to detect turn completion via desireState probabilities.
Args:
modelv2: ModelV2 message
desire_state: Model's desire state output
"""
# Reserved for future use - detecting turn completion via model output
pass
+919
View File
@@ -0,0 +1,919 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MAA Control Daemon - Turn Assist with Dead Reckoning
================================================================================
OVERVIEW
================================================================================
This daemon provides high-frequency (20Hz) turn assist signals for navigation.
The key insight is that navInstruction updates at only 1Hz (too slow for smooth
turn tracking), so we use "snapshot and coast" - capture turn info once, then
dead reckon using the car's own sensors.
Subscribes to:
- navInstruction: turn info from maad (1Hz) - used as TRIGGER only
- liveGPS: position, bearing (1Hz)
- navRoute: route geometry - reset on route change
- carState: vEgo, blinker, yawRate (100Hz) - for dead reckoning
Publishes:
- maaControl: turnDistance, turnDirection, desireActive, turnState, etc.
================================================================================
DEAD RECKONING APPROACH
================================================================================
Problem:
Navigation updates come in slow (1Hz). At 60 km/h, that's 17m between updates.
This causes jerky distance countdown and imprecise turn detection.
Solution: "Snapshot and Coast"
1. TRIGGER at ~200m: capture turn params (angle, distance, direction)
2. IGNORE subsequent navInstruction updates for this turn
3. DEAD RECKON distance: integrate vEgo from carState (100Hz)
4. TRACK HEADING: integrate yawRate from carState during turn execution
Distance remaining = initial_distance - (vEgo × dt)
Heading change = (yawRate × dt)
================================================================================
STATE MACHINE
================================================================================
NONE
navInstruction shows turn within 200m
(significant angle 20°, has direction)
APPROACHING
Show turn suggestion at <150m
Wait for driver blinker (acknowledgment)
Dead reckon distance using vEgo
NOT tracking yaw yet (allows overtaking)
At <100m WITH blinker: COMMIT
blockLaneChange = true
speedLimitActive = true
desireActive = true
Dead reckoned distance < 30m
EXECUTING
Now tracking yaw (accumulated heading change)
Compare accumulated vs expected turn angle
desireActive = true (only if acknowledged)
|accumulated_yaw| >= |expected_angle| - tolerance
COMPLETE (2s cooldown) NONE
MISSED (any abort condition)
Wait for route change (navRoute coords change)
NONE
================================================================================
DRIVER ACKNOWLEDGMENT (Two-Step, Like Lane Change)
================================================================================
Two confirmation steps:
1. Blinker matching turn direction = approach confirmed (speed limit, block lane change)
2. Steering torque in turn direction = turn execution confirmed (desire sent)
Blinker is the main confirmation for slowing down. Steering is required to
actually send the turn desire to the model (like lane change).
Distance | Without Blinker | With Blinker | + Steering
-----------|--------------------------|----------------------------|------------------
200m-150m | Informational only | Informational only | (same)
150m-100m | Show suggestion | speedLimitActive = true | (same)
<100m | Show suggestion | slow down, block LC | + desireActive
<30m | Enter EXECUTING | slow down, block LC | + desireActive
Key behaviors:
- speedLimitActive: When blinker on, enforce turn speed limit
- blockLaneChange: At <100m with blinker, block lane change desire
- desireActive: Only sent when blinker AND steering confirmed (at turn)
If driver has blinker but doesn't steer:
- System slows down for the turn
- But doesn't send turn desire (driver steers manually)
- Once driver steers in turn direction, desire activates
================================================================================
ABORT/MISS DETECTION
================================================================================
Driver can override at any time. We detect "missed turn" via:
1. DROVE TOO FAR (no turn)
- Condition: distance_traveled > 2× initial_distance
- Meaning: drove way past expected turn point without turning
- Example: triggered at 200m, drove 400m, barely any yaw change
2. WRONG DIRECTION
- Condition: accumulated_yaw > 20° in opposite direction
- Meaning: driver turned the wrong way
- Example: expected right turn, driver turned 25° left
3. INSUFFICIENT TURN
- Condition: drove 2× total distance but <30% of expected yaw
- Meaning: drove through intersection without completing turn
- Example: expected 90° turn, only turned 20° after driving 500m
4. TIMEOUT
- Condition: 30 seconds of MOVING time (v_ego > 1 m/s)
- Meaning: something went wrong, taking too long
- Note: stopped time (traffic light) doesn't count
5. PASSED TURN (nav jumped to next)
- Condition: dead_reckon < 50m but nav says > 150m
- Meaning: nav is now showing NEXT turn, we passed this one
- Example: we think 30m to turn, nav says 400m (next turn)
6. DIRECTION CHANGED
- Condition: turnAngle sign flipped (left right)
- Meaning: route recalculated or nav corrected itself
- Example: was +90° (left), now -45° (right)
7. TURN DISAPPEARED
- Condition: turnAngle dropped below 20° threshold
- Meaning: no longer a significant turn (route changed or passed)
================================================================================
DRIVER OVERRIDE SCENARIOS
================================================================================
Overtaking another car:
- During APPROACHING: No problem! We only track distance, not yaw.
Driver can swerve left/right to overtake, doesn't affect tracking.
- During EXECUTING: Brief swerves (<20°) won't trigger wrong direction.
Only sustained opposite turn triggers abort.
Stopping at traffic light:
- moving_time only increments when v_ego > 1 m/s
- Can wait indefinitely at red light without timeout
- Distance tracking pauses when stopped (vEgo 0)
Taking a different route intentionally:
- System correctly detects as MISSED
- Waits for navRoute to change (reroute)
- Then ready to track new turn
================================================================================
DATA FLOW
================================================================================
navInstruction (1Hz) carState (100Hz)
turnAngle, maneuverDistance vEgo, yawRate, blinker
maneuverType, modifier
TurnTracker
trigger() at 200m, capture params
update(vEgo, yawRate) every 50ms (20Hz)
distance_traveled += vEgo × dt
accumulated_yaw += yawRate × dt (if EXECUTING)
check abort conditions
maaControl (20Hz)
turnDistance (dead reckoned)
turnState (NONE/APPROACHING/EXECUTING/COMPLETE/MISSED)
desireActive (for blinker/lane change desire)
turnProgress (accumulated yaw in degrees)
================================================================================
CONFIGURATION
================================================================================
TURN_TRIGGER_DISTANCE = 200m # Start dead reckoning
TURN_DESIRE_DISTANCE = 150m # Show turn suggestion, wait for blinker
TURN_COMMIT_DISTANCE = 100m # With blinker: commit (block lane change, slow)
TURN_EXECUTE_DISTANCE = 30m # Start tracking yaw
TURN_ANGLE_TOLERANCE = 15° # Turn complete within this
TURN_MIN_ANGLE = 20° # Minimum to be "significant"
TURN_TIMEOUT = 30s # Max moving time
================================================================================
OUTPUT FIELDS (maaControl)
================================================================================
turnDistance - Dead reckoned distance to turn (m)
turnDirection - left/right/none
turnAngle - Expected turn angle (deg, + = left)
turnState - 0=none, 1=approaching, 2=executing, 3=complete, 4=missed
turnProgress - Accumulated yaw during turn (deg)
desireActive - Send turn desire to model (blinker + steering confirmed)
driverAcknowledged - Driver turned on matching blinker
speedLimitActive - Enforce turn speed limit (blinker on)
blockLaneChange - Block lane change desire (blinker + committed)
turnSpeedLimit - Target speed for turn (m/s)
================================================================================
"""
import json
import math
import time
from enum import IntEnum
import numpy as np
from cereal import messaging, log, custom
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
from dragonpilot.dashy.maa.helpers import (
Coordinate,
compute_path_curvature,
find_closest_point_on_route,
)
# CarrotPilot curvature-to-speed lookup table
# Adapted from https://github.com/ajouatom/openpilot
# Maps curvature (1/m) to recommended speed (km/h)
# Based on physics: v = sqrt(a_lat / κ) where a_lat ≈ 2.5 m/s² for comfort
V_CURVE_LOOKUP_BP = [0., 1./800., 1./670., 1./560., 1./440., 1./360., 1./265., 1./190., 1./135., 1./85., 1./55., 1./30., 1./25.]
V_CURVE_LOOKUP_VALS = [300., 150., 120., 110., 100., 90., 80., 70., 60., 50., 40., 15., 5.] # km/h
# Configuration
CURVATURE_ASSIST_ENABLED = False # Disable continuous curvature steering assist
CURVATURE_LOOKAHEAD = 2.5 # seconds ahead for curvature calculation
TURN_VALID_DISTANCE = 500.0 # meters - turn is valid if within this distance
MIN_SPEED_FOR_CURVATURE = 1.0 # m/s - minimum speed to use curvature
# Turn execution - dead reckoning based
TURN_TRIGGER_DISTANCE = 200.0 # meters - capture turn info and start dead reckoning
TURN_DESIRE_DISTANCE = 150.0 # meters - show turn suggestion, wait for blinker
TURN_COMMIT_DISTANCE = 100.0 # meters - if blinker on, commit (block lane change, slow down)
TURN_EXECUTE_DISTANCE = 30.0 # meters - start tracking heading (entering intersection)
TURN_ANGLE_TOLERANCE = 15.0 # degrees - turn complete when within this of target
TURN_MIN_ANGLE = 20.0 # degrees - minimum angle to consider a "turn"
# Abort detection thresholds
TURN_MISS_DISTANCE_FACTOR = 2.0 # drove 2x expected distance without turning = missed
TURN_MISS_YAW_THRESHOLD = 0.3 # must achieve at least 30% of turn angle
TURN_WRONG_DIRECTION_ANGLE = 20.0 # degrees in wrong direction = missed
TURN_TIMEOUT = 30.0 # seconds - max time in APPROACHING/EXECUTING
class TurnState(IntEnum):
NONE = 0 # no turn pending
APPROACHING = 1 # turn ahead, dead reckoning distance
EXECUTING = 2 # in intersection, tracking heading change
COMPLETE = 3 # turn done, cooldown before next
MISSED = 4 # turn missed/aborted, wait for reroute
class TurnTracker:
"""
Tracks turn execution using dead reckoning.
Once triggered at ~200m, ignores navInstruction updates and purely
uses vEgo (distance) and yawRate (heading) from carState.
Driver acknowledgment flow (two-step, like lane change):
- System shows turn suggestion at 150m
- Driver turns on blinker = acknowledged (speed limit, block lane change)
- Driver steers in turn direction = steering confirmed (desire sent)
- Blinker gates the approach, steering gates the turn execution
"""
def __init__(self):
self.state = TurnState.NONE
# Captured at trigger
self.expected_angle = 0.0 # degrees (positive=left, negative=right)
self.initial_distance = 0.0 # distance to turn when triggered
self.direction = 'none' # 'left' or 'right'
self.maneuver_type = 0 # MaaControl.ManeuverType
self.turn_speed_limit = 0.0 # m/s
# Dead reckoning state
self.distance_traveled = 0.0 # meters since trigger
self.accumulated_yaw = 0.0 # degrees turned since execute start
self.moving_time = 0.0 # accumulated time while moving (for timeout)
self.execute_start_time = 0.0 # when EXECUTING started
self.complete_time = 0.0 # when turn completed
self.last_update_time = 0.0 # for dt calculation
# Driver acknowledgment (two-step like lane change)
self.driver_acknowledged = False # blinker matches turn direction
self.steering_confirmed = False # steering torque matches turn direction
def reset(self):
self.state = TurnState.NONE
self.expected_angle = 0.0
self.initial_distance = 0.0
self.direction = 'none'
self.maneuver_type = 0
self.turn_speed_limit = 0.0
self.distance_traveled = 0.0
self.accumulated_yaw = 0.0
self.moving_time = 0.0
self.execute_start_time = 0.0
self.complete_time = 0.0
self.last_update_time = 0.0
self.driver_acknowledged = False
self.steering_confirmed = False
def get_estimated_distance(self) -> float:
"""Get estimated distance to turn based on dead reckoning."""
if self.state in (TurnState.APPROACHING, TurnState.EXECUTING):
return max(0.0, self.initial_distance - self.distance_traveled)
return 9999.0
def trigger(self, t: float, turn_distance: float, turn_angle: float,
maneuver_type: int, turn_direction: int, turn_speed_limit: float):
"""Capture turn parameters and start dead reckoning."""
self.state = TurnState.APPROACHING
self.expected_angle = turn_angle
self.initial_distance = turn_distance
self.maneuver_type = maneuver_type
self.turn_speed_limit = turn_speed_limit
self.direction = 'left' if turn_direction == custom.MaaControl.TurnDirection.left else 'right'
self.distance_traveled = 0.0
self.accumulated_yaw = 0.0
self.moving_time = 0.0
self.last_update_time = t
cloudlog.info(f"maa: turn triggered, angle={turn_angle:.1f}°, dist={turn_distance:.0f}m, dir={self.direction}")
def check_blinker(self, left_blinker: bool, right_blinker: bool):
"""Check if driver turned on blinker matching turn direction."""
if self.direction == 'left' and left_blinker:
self.driver_acknowledged = True
elif self.direction == 'right' and right_blinker:
self.driver_acknowledged = True
# Note: we don't reset acknowledged if blinker turns off
# (driver may have tapped blinker briefly to acknowledge)
def check_steering(self, steering_pressed: bool, steering_torque: float):
"""Check if driver is steering in the turn direction (like lane change confirmation)."""
if not steering_pressed:
return
# Same logic as lane change: positive torque = left, negative = right
if self.direction == 'left' and steering_torque > 0:
self.steering_confirmed = True
elif self.direction == 'right' and steering_torque < 0:
self.steering_confirmed = True
# Once confirmed, stays confirmed (one-time check)
def update(self, t: float, v_ego: float, yaw_rate: float) -> bool:
"""
Update turn state with dead reckoning.
Args:
t: current time (monotonic)
v_ego: vehicle speed (m/s) from carState
yaw_rate: yaw rate (rad/s) from carState
Returns True if desire should be sent (acknowledged + within commit distance).
"""
if self.state == TurnState.NONE:
return False
# Calculate dt
dt = t - self.last_update_time if self.last_update_time > 0 else 0.05
dt = min(dt, 0.2) # Cap dt to handle timing glitches
self.last_update_time = t
# Integrate distance traveled (and moving time for timeout)
self.distance_traveled += v_ego * dt
if v_ego > 1.0: # Only count time when actually moving
self.moving_time += dt
estimated_dist = self.get_estimated_distance()
# Timeout check - based on moving time (allows waiting at traffic lights)
if self.moving_time > TURN_TIMEOUT:
cloudlog.warning(f"maa: turn timeout after {self.moving_time:.0f}s moving")
self.state = TurnState.MISSED
return False
if self.state == TurnState.APPROACHING:
# Check if we've reached the intersection
if estimated_dist <= TURN_EXECUTE_DISTANCE:
self.state = TurnState.EXECUTING
self.execute_start_time = t
self.accumulated_yaw = 0.0
cloudlog.info(f"maa: turn executing, traveled={self.distance_traveled:.0f}m")
# Miss detection: drove way past without entering execute
if self.distance_traveled > self.initial_distance * TURN_MISS_DISTANCE_FACTOR:
cloudlog.warning(f"maa: turn missed (drove past), traveled={self.distance_traveled:.0f}m")
self.state = TurnState.MISSED
return False
elif self.state == TurnState.EXECUTING:
# Integrate yaw rate (convert rad/s to deg)
yaw_change_deg = math.degrees(yaw_rate) * dt
self.accumulated_yaw += yaw_change_deg
# Fix 3: Early abort if going straight through the "turn" (map error)
# If we've traveled 20m past the turn point with no significant yaw change,
# the turn doesn't exist - abort quickly instead of extended false braking
distance_past_turn = self.distance_traveled - self.initial_distance
if distance_past_turn > 20.0 and abs(self.accumulated_yaw) < 5.0:
cloudlog.warning(f"maa: no turn detected (past {distance_past_turn:.0f}m, yaw={self.accumulated_yaw:.1f}°), aborting")
self.state = TurnState.MISSED
return False
# Check completion: achieved target heading
# For left turn: expected_angle > 0, accumulated should go positive
# For right turn: expected_angle < 0, accumulated should go negative
progress = abs(self.accumulated_yaw) / abs(self.expected_angle) if self.expected_angle != 0 else 0
angle_remaining = abs(self.expected_angle) - abs(self.accumulated_yaw)
if angle_remaining <= TURN_ANGLE_TOLERANCE:
self.state = TurnState.COMPLETE
self.complete_time = t
cloudlog.info(f"maa: turn complete, yaw={self.accumulated_yaw:.1f}°, expected={self.expected_angle:.1f}°")
return False
# Wrong direction detection: significant yaw in opposite direction
if self.expected_angle > 0 and self.accumulated_yaw < -TURN_WRONG_DIRECTION_ANGLE:
cloudlog.warning(f"maa: turn wrong direction (expected left, went right)")
self.state = TurnState.MISSED
return False
if self.expected_angle < 0 and self.accumulated_yaw > TURN_WRONG_DIRECTION_ANGLE:
cloudlog.warning(f"maa: turn wrong direction (expected right, went left)")
self.state = TurnState.MISSED
return False
# Miss detection: drove 2x expected total distance without sufficient turn
total_expected = self.initial_distance + 50.0 # Add some buffer for turn itself
if self.distance_traveled > total_expected * TURN_MISS_DISTANCE_FACTOR:
if progress < TURN_MISS_YAW_THRESHOLD:
cloudlog.warning(f"maa: turn missed (insufficient yaw), progress={progress:.1%}")
self.state = TurnState.MISSED
return False
elif self.state == TurnState.COMPLETE:
# Short cooldown then reset
if t - self.complete_time > 2.0:
self.reset()
return False
elif self.state == TurnState.MISSED:
# Stay in missed state until route changes (reset called externally)
return False
# Desire active when:
# Driver acknowledged (blinker on) AND steering confirmed AND either:
# 1. APPROACHING and within commit distance (<100m), OR
# 2. EXECUTING (in the turn)
# Without both confirmations, NO desire is sent (driver maintains control)
if not self.driver_acknowledged or not self.steering_confirmed:
return False
if self.state == TurnState.APPROACHING:
return estimated_dist <= TURN_COMMIT_DISTANCE
if self.state == TurnState.EXECUTING:
return True
return False
def get_turn_speed_from_curvature(curvature: float) -> float:
"""
Compute recommended speed for turn using CarrotPilot's curvature lookup table.
This is physics-based: v = sqrt(a_lat / κ) where a_lat 2.5 m/ for comfort.
Args:
curvature: Road curvature in 1/m (positive or negative)
Returns:
Recommended speed in m/s
"""
abs_curv = abs(curvature)
speed_kph = np.interp(abs_curv, V_CURVE_LOOKUP_BP, V_CURVE_LOOKUP_VALS)
return speed_kph / 3.6 # Convert to m/s
def get_turn_speed_limit(modifier: str, turn_angle: float, nav_type: str = '', curvature: float = 0.0) -> float:
"""
Compute recommended speed for turn based on curvature (preferred) or heuristics.
Uses CarrotPilot's curvature lookup table when curvature is available.
Falls back to heuristic-based calculation when curvature is not available.
Args:
modifier: OSRM maneuver modifier (e.g., 'left', 'right', 'sharp left')
turn_angle: Turn angle in degrees (positive=left, negative=right)
nav_type: OSRM maneuver type (e.g., 'turn', 'off ramp')
curvature: Road curvature in 1/m (from geometry calculation)
Returns:
Recommended speed in m/s
"""
# Use curvature-based calculation when curvature is available
# This is more accurate than heuristics as it's physics-based
if abs(curvature) > 0.001: # Meaningful curvature
speed_from_curv = get_turn_speed_from_curvature(curvature)
# Clamp to reasonable range: 5 km/h to 100 km/h for turns
return max(5.0 / 3.6, min(100.0 / 3.6, speed_from_curv))
# Fallback: Heuristic-based calculation when curvature not available
# Highway maneuvers (exits, ramps, merges) - much higher speeds
if nav_type in ('depart', 'off ramp', 'on ramp', 'merge', 'fork'):
if 'sharp' in modifier:
base_speed = 50.0 # km/h - sharp highway exit
elif 'slight' in modifier:
base_speed = 80.0 # km/h - gentle curve
else:
base_speed = 60.0 # km/h - normal exit ramp
return base_speed / 3.6 # Don't apply angle adjustments to highway maneuvers
# Intersection turns - lower speeds
if 'sharp' in modifier:
base_speed = 15.0 # km/h
elif 'slight' in modifier:
base_speed = 45.0 # km/h
elif modifier in ('left', 'right'):
base_speed = 25.0 # km/h
elif 'uturn' in modifier:
base_speed = 10.0 # km/h
else:
base_speed = 50.0 # km/h default
# Adjust based on actual angle if available (only for intersection turns)
abs_angle = abs(turn_angle)
if abs_angle > 90:
base_speed = min(base_speed, 15.0)
elif abs_angle > 60:
base_speed = min(base_speed, 25.0)
elif abs_angle > 30:
base_speed = min(base_speed, 35.0)
return base_speed / 3.6 # Convert to m/s
def get_maneuver_type(nav_type: str, turn_angle: float = 0.0) -> int:
"""Determine maneuver type primarily from geometry, with OSRM hints.
Geometry is the source of truth - OSRM labels can be wrong.
"""
# Geometry-first: significant turn angle = it's a turn
if abs(turn_angle) >= TURN_MIN_ANGLE:
return custom.MaaControl.ManeuverType.turn
# Highway maneuvers (even with small angle) - use laneChange desire
if nav_type in ('off ramp', 'on ramp', 'merge', 'fork'):
return custom.MaaControl.ManeuverType.laneChange
# OSRM says turn but geometry is minor - still treat as turn
if nav_type in ('turn', 'end of road'):
return custom.MaaControl.ManeuverType.turn
# Everything else: no action needed
return custom.MaaControl.ManeuverType.none
def get_last_gps_position(params: Params) -> tuple:
"""Get last known GPS position from params. Returns (lat, lon, bearing) or None."""
try:
data = params.get("LastGPSPosition")
if data:
pos = json.loads(data)
return pos.get('latitude'), pos.get('longitude'), pos.get('bearing', 0.0)
except Exception:
pass
return None
def main():
cloudlog.info("maa_controld: starting")
params = Params()
sm = messaging.SubMaster(
['navRoute', 'navInstruction', 'liveGPS', 'carState'],
ignore_alive=['navRoute', 'navInstruction', 'liveGPS', 'carState']
)
pm = messaging.PubMaster(['maaControl'])
rk = Ratekeeper(20)
# State
route_coords: list[Coordinate] = []
last_route_len = 0
last_gps_time = 0.0
closest_idx = 0
# Turn tracker (dead reckoning based)
turn_tracker = TurnTracker()
# Fallback position from params (used before liveGPS is ready)
fallback_pos = get_last_gps_position(params)
if fallback_pos:
cloudlog.info(f"maa_controld: fallback position {fallback_pos[0]:.6f}, {fallback_pos[1]:.6f}")
while True:
sm.update(0)
t = time.monotonic()
# Get current position - prefer liveGPS, fall back to LastGPSPosition
if sm.updated['liveGPS']:
last_gps_time = time.monotonic()
gps = sm['liveGPS']
gps_stale = (time.monotonic() - last_gps_time) > 2.0
gps_valid = gps.status == custom.LiveGPS.Status.valid and not gps_stale
# Use fallback if liveGPS not ready
use_fallback = not gps_valid and fallback_pos is not None
if gps_valid:
current_lat = gps.latitude
current_lon = gps.longitude
current_bearing = gps.bearingDeg
elif use_fallback:
current_lat, current_lon, current_bearing = fallback_pos
else:
current_lat = current_lon = current_bearing = None
# Get turn info from navInstruction
nav = sm['navInstruction']
nav_valid = sm.valid['navInstruction']
# Always update route coordinates (needed for turn angle calculation)
nav_route = sm['navRoute']
if sm.valid['navRoute'] and nav_route.coordinates:
new_len = len(nav_route.coordinates)
if new_len != last_route_len:
route_coords = [
Coordinate(c.latitude, c.longitude)
for c in nav_route.coordinates
]
last_route_len = new_len
closest_idx = 0
turn_tracker.reset() # Reset turn state when route changes
cloudlog.debug(f"maa_controld: route updated, {new_len} points")
# Find current position on route
has_position = current_lat is not None
if route_coords and has_position:
current_pos = Coordinate(current_lat, current_lon)
try:
# Optimization: Search locally around last known position
search_start = max(0, closest_idx - 10)
search_end = min(len(route_coords), closest_idx + 50)
if closest_idx == 0:
search_start = 0
search_end = len(route_coords)
subset = route_coords[search_start:search_end]
local_idx, _ = find_closest_point_on_route(current_pos, subset)
closest_idx = search_start + local_idx
except Exception as e:
cloudlog.warning(f"maa_controld: position error: {e}")
# Get speed, blinker, and yawRate from carState
cs = sm['carState']
cs_valid = sm.valid['carState']
v_ego = cs.vEgo if cs_valid else 0.0
left_blinker = cs.leftBlinker if cs_valid else False
right_blinker = cs.rightBlinker if cs_valid else False
yaw_rate = cs.yawRate if cs_valid else 0.0
# Continuous curvature assist (optional - for steering)
curvature = 0.0
curvature_valid = False
if CURVATURE_ASSIST_ENABLED and route_coords and has_position and v_ego > MIN_SPEED_FOR_CURVATURE:
try:
curvature = compute_path_curvature(
current_pos, current_bearing, route_coords, closest_idx, v_ego, CURVATURE_LOOKAHEAD
)
curvature_valid = True
except Exception as e:
cloudlog.warning(f"maa_controld: curvature error: {e}")
# Build maaControl message
msg = messaging.new_message('maaControl', valid=True)
maa = msg.maaControl
maa.curvature = float(curvature)
maa.curvatureValid = curvature_valid
# Get maneuver info from navInstruction (1Hz, used only for trigger)
maneuver_dist = getattr(nav, 'maneuverDistance', None) if nav_valid else None
# If turn tracker is active (including MISSED/COMPLETE), handle accordingly
if turn_tracker.state in (TurnState.APPROACHING, TurnState.EXECUTING):
# Safety checks: detect if turn info changed significantly
if nav_valid and maneuver_dist is not None:
# Use turnAngle from navInstructionExt - this is geometry-based (reliable)
turn_angle = getattr(nav, 'turnAngle', 0.0) or 0.0
estimated_dist = turn_tracker.get_estimated_distance()
# Check 1: Did we pass the turn? (nav distance jumped up = now showing NEXT turn)
# If we think we're close (<50m) but nav says far (>150m), we probably passed it
if estimated_dist < 50.0 and maneuver_dist > 150.0:
cloudlog.warning(f"maa: likely passed turn (est={estimated_dist:.0f}m, nav={maneuver_dist:.0f}m), resetting")
turn_tracker.reset()
# Check 2: Did direction flip? (route recalculated or now showing different turn)
else:
current_nav_dir = None
if turn_angle > TURN_MIN_ANGLE:
current_nav_dir = 'left'
elif turn_angle < -TURN_MIN_ANGLE:
current_nav_dir = 'right'
if current_nav_dir and current_nav_dir != turn_tracker.direction:
cloudlog.warning(f"maa: turn direction changed ({turn_tracker.direction}{current_nav_dir}), angle={turn_angle:.1f}°, resetting")
turn_tracker.reset()
# Check 3: Turn disappeared (angle now below threshold)
elif abs(turn_angle) < TURN_MIN_ANGLE:
nav_type = getattr(nav, 'maneuverType', '') or ''
if get_maneuver_type(nav_type, turn_angle) == custom.MaaControl.ManeuverType.none:
cloudlog.warning(f"maa: turn no longer valid (angle={turn_angle:.1f}°), resetting")
turn_tracker.reset()
if turn_tracker.state in (TurnState.APPROACHING, TurnState.EXECUTING):
# Check blinker and steering for driver acknowledgment (like lane change)
turn_tracker.check_blinker(left_blinker, right_blinker)
turn_tracker.check_steering(cs.steeringPressed, cs.steeringTorque)
# Dead reckon distance - ignore navInstruction updates
estimated_dist = turn_tracker.get_estimated_distance()
# Fix 2: Allow abort if blinker turns off before commitment (>100m)
# User can change their mind if not yet committed to the turn
if turn_tracker.state == TurnState.APPROACHING and turn_tracker.driver_acknowledged:
blinker_matches = (turn_tracker.direction == 'left' and left_blinker) or \
(turn_tracker.direction == 'right' and right_blinker)
if not blinker_matches and estimated_dist > TURN_COMMIT_DISTANCE:
turn_tracker.driver_acknowledged = False
turn_tracker.steering_confirmed = False
cloudlog.info("maa: blinker canceled before commit, aborting turn assist")
maa.turnDistance = float(estimated_dist)
maa.turnValid = True
maa.turnAngle = float(turn_tracker.expected_angle)
maa.maneuverType = turn_tracker.maneuver_type
maa.turnSpeedLimit = float(turn_tracker.turn_speed_limit)
# Direction from captured state
if turn_tracker.direction == 'left':
maa.turnDirection = custom.MaaControl.TurnDirection.left
elif turn_tracker.direction == 'right':
maa.turnDirection = custom.MaaControl.TurnDirection.right
else:
maa.turnDirection = custom.MaaControl.TurnDirection.none
# Update tracker with dead reckoning
desire_active = turn_tracker.update(t, v_ego, yaw_rate)
maa.turnState = int(turn_tracker.state)
maa.turnProgress = float(turn_tracker.accumulated_yaw)
# Driver acknowledgment status
maa.driverAcknowledged = turn_tracker.driver_acknowledged
# Speed limit active when blinker on (driver acknowledged)
maa.speedLimitActive = turn_tracker.driver_acknowledged
# Block lane change when committed (blinker + within commit distance)
maa.blockLaneChange = turn_tracker.driver_acknowledged and estimated_dist <= TURN_COMMIT_DISTANCE
# desireActive: send turn desire to model (requires blinker)
maa.desireActive = desire_active
# Curvature from captured state (not recalculated)
maa.curvature = 0.0
maa.curvatureValid = False
maa.turnCurvature = 0.0
elif nav_valid and maneuver_dist is not None:
# No active turn tracking - check if we should trigger
maa.turnDistance = float(maneuver_dist)
maa.turnValid = maneuver_dist < TURN_VALID_DISTANCE
nav_type = getattr(nav, 'maneuverType', '') or ''
modifier = getattr(nav, 'maneuverModifier', '') or ''
# Get pre-computed turn geometry from navInstruction
turn_angle = getattr(nav, 'turnAngle', 0.0) or 0.0
turn_curvature = getattr(nav, 'turnCurvature', 0.0) or 0.0
maa.turnAngle = float(turn_angle)
maa.turnCurvature = float(turn_curvature)
# Compute maneuver type
maa.maneuverType = get_maneuver_type(nav_type, turn_angle)
# Set turn direction
if maa.maneuverType != custom.MaaControl.ManeuverType.none:
if 'left' in modifier.lower():
maa.turnDirection = custom.MaaControl.TurnDirection.left
elif 'right' in modifier.lower():
maa.turnDirection = custom.MaaControl.TurnDirection.right
elif turn_angle > TURN_MIN_ANGLE:
maa.turnDirection = custom.MaaControl.TurnDirection.left
elif turn_angle < -TURN_MIN_ANGLE:
maa.turnDirection = custom.MaaControl.TurnDirection.right
else:
maa.turnDirection = custom.MaaControl.TurnDirection.none
else:
maa.turnDirection = custom.MaaControl.TurnDirection.none
# Compute turn speed limit using CarrotPilot curvature lookup table
# Curvature-based is more accurate than heuristics when available
turn_speed_limit = get_turn_speed_limit(modifier, turn_angle, nav_type, turn_curvature)
maa.turnSpeedLimit = float(turn_speed_limit)
# Check if we should trigger dead reckoning
# Don't trigger if already in MISSED/COMPLETE state (wait for route change)
can_trigger = turn_tracker.state == TurnState.NONE
is_actionable = maa.maneuverType != custom.MaaControl.ManeuverType.none
has_direction = maa.turnDirection != custom.MaaControl.TurnDirection.none
is_significant = abs(turn_angle) >= TURN_MIN_ANGLE
in_trigger_range = maneuver_dist <= TURN_TRIGGER_DISTANCE
if can_trigger and is_actionable and has_direction and is_significant and in_trigger_range:
# Trigger! Capture turn params and start dead reckoning
turn_tracker.trigger(
t, maneuver_dist, turn_angle,
maa.maneuverType, maa.turnDirection, turn_speed_limit
)
# Check blinker and steering immediately after trigger
turn_tracker.check_blinker(left_blinker, right_blinker)
turn_tracker.check_steering(cs.steeringPressed, cs.steeringTorque)
# Blinker = approach confirmation, steering = turn execution confirmation
maa.desireActive = turn_tracker.driver_acknowledged and turn_tracker.steering_confirmed and maneuver_dist <= TURN_COMMIT_DISTANCE
maa.turnState = int(turn_tracker.state)
maa.turnProgress = 0.0
maa.driverAcknowledged = turn_tracker.driver_acknowledged
maa.speedLimitActive = turn_tracker.driver_acknowledged
maa.blockLaneChange = turn_tracker.driver_acknowledged and maneuver_dist <= TURN_COMMIT_DISTANCE
elif turn_tracker.state in (TurnState.COMPLETE, TurnState.MISSED):
# In cooldown - call update to handle state transitions
turn_tracker.update(t, v_ego, yaw_rate)
maa.desireActive = False
maa.turnState = int(turn_tracker.state)
maa.turnProgress = float(turn_tracker.accumulated_yaw)
maa.driverAcknowledged = False
maa.speedLimitActive = False
maa.blockLaneChange = False
else:
# Not triggered yet (turn too far away)
maa.desireActive = False
maa.turnState = int(TurnState.NONE)
maa.turnProgress = 0.0
maa.driverAcknowledged = False
maa.speedLimitActive = False
maa.blockLaneChange = False
else:
# No valid nav instruction
maa.turnValid = False
maa.turnDirection = custom.MaaControl.TurnDirection.none
maa.turnSpeedLimit = 50.0 / 3.6
maa.maneuverType = custom.MaaControl.ManeuverType.none
maa.turnAngle = 0.0
maa.turnCurvature = 0.0
maa.turnDistance = 9999.0
maa.desireActive = False
maa.turnState = int(TurnState.NONE)
maa.turnProgress = 0.0
maa.driverAcknowledged = False
maa.speedLimitActive = False
maa.blockLaneChange = False
# Only reset if not in MISSED state (wait for route change)
if turn_tracker.state != TurnState.MISSED:
turn_tracker.reset()
pm.send('maaControl', msg)
rk.keep_time()
if __name__ == "__main__":
main()
+566
View File
@@ -0,0 +1,566 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dragonpilot Map-Aware Assist Daemon (maad)
Handles route calculation and navigation instructions using OSRM routing.
Similar to openpilot's navd but uses free OSRM API instead of Mapbox.
Control signals (maaControl) are published by maa_controld.py separately
for low-latency, deterministic control.
Flow:
1. Reads dp_maa_destination from params (set by dashy)
2. Fetches route from OSRM (free routing API) - async to avoid blocking
3. Subscribes to liveGPS for position updates
4. Publishes navInstruction (for UI) and navRoute (for map display)
"""
import json
import time
import threading
import queue
from dataclasses import dataclass
from typing import Optional
import cereal.messaging as messaging
from cereal import custom, log
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
from dragonpilot.dashy.maa.helpers import (
Coordinate,
coordinate_from_param,
find_closest_point_on_route,
distance_along_geometry,
compute_turn_angle_at_index,
compute_turn_curvature_at_index,
)
from dragonpilot.dashy.maa.providers import MapService
from dragonpilot.dashy.maa.providers.models import Coordinate as ProviderCoordinate
from dragonpilot.dashy.maa.route_tracker import RouteTracker, REROUTE_DISTANCE_BASE, REROUTE_DEBOUNCE_TIME
MANEUVER_TRANSITION_THRESHOLD = 10 # meters past maneuver to transition
NAV_RATE = 1.0 # Hz - navigation update rate
@dataclass
class Step:
"""Represents a navigation step/segment."""
distance: float # total distance of step in meters
duration: float # duration in seconds
duration_typical: Optional[float]
name: str
maneuver_type: str
maneuver_modifier: str
geometry: list[Coordinate] # coordinates for this step
speed_limit: Optional[float] # m/s
speed_limit_sign: str # 'mutcd' or 'vienna'
# Pre-computed turn geometry (computed once when route is fetched)
turn_angle: float = 0.0 # degrees, positive=left, negative=right
turn_curvature: float = 0.0 # 1/m, positive=left, negative=right
# Explicit maneuver point from OSRM (for fork/turn detection)
maneuver_point: Optional[Coordinate] = None
class RouteEngine:
def __init__(self, sm: messaging.SubMaster, pm: messaging.PubMaster):
self.sm = sm
self.pm = pm
self.params = Params()
self.map_service = MapService(self.params)
# Get last GPS position from params
self.last_position = coordinate_from_param("LastGPSPosition", self.params)
if self.last_position is None:
# Default to Taipei 101 for bench testing
self.last_position = Coordinate(25.033976, 121.564472)
self.last_bearing: Optional[float] = None
self.gps_ok = False
self.gps_speed = 0.0
self.gps_accuracy = 0.0 # horizontal accuracy in meters
self.last_gps_time = 0.0
# Route state
self.nav_destination: Optional[Coordinate] = None
self.route: Optional[list[Step]] = None
self.tracker = RouteTracker() # OsmAnd-style route tracking
# Recompute state
self.recompute_backoff = 0
self.recompute_countdown = 0
# Async route calculation
self._route_queue: queue.Queue = queue.Queue()
self._route_thread: Optional[threading.Thread] = None
self._route_calculating = False
# Timing diagnostics
self._frame_count = 0
self._last_timing_log = time.monotonic()
self._gps_save_counter = 0
self._route_send_counter = 0
# Params cache - avoid reading params every frame
self._cached_destination: Optional[Coordinate] = None
self._last_destination_check = 0.0
self._destination_check_interval = 3.0 # seconds
# First valid GPS flag - triggers immediate route calculation
self._had_first_valid_gps = False
def update(self):
t0 = time.monotonic()
self.sm.update(0)
t1 = time.monotonic()
self._update_location()
self._check_route_result() # Non-blocking check for async route result
t2 = time.monotonic()
try:
self._recompute_route()
t3 = time.monotonic()
self._send_instruction()
# Resend route periodically (every 1s) so new UI clients can see it
if self.route is not None:
self.send_route()
t4 = time.monotonic()
except Exception:
cloudlog.exception("maad.failed_to_compute")
t3 = t4 = time.monotonic()
# Log timing every 10 seconds
self._frame_count += 1
total_time = t4 - t0
if total_time > 0.05: # Log if frame took > 50ms
cloudlog.warning(f"maad slow frame: total={total_time*1000:.0f}ms "
f"(sm={1000*(t1-t0):.0f}, loc={1000*(t2-t1):.0f}, "
f"route={1000*(t3-t2):.0f}, instr={1000*(t4-t3):.0f})")
if t4 - self._last_timing_log > 10.0:
actual_rate = self._frame_count / (t4 - self._last_timing_log)
cloudlog.info(f"maad rate: {actual_rate:.1f} Hz (target {NAV_RATE} Hz), frames={self._frame_count}")
self._frame_count = 0
self._last_timing_log = t4
def _check_route_result(self):
"""Check for async route calculation result (non-blocking)."""
try:
result = self._route_queue.get_nowait()
self._route_calculating = False
if result is None:
# Route calculation failed
self._clear_route(clear_destination=False)
return
# Apply the calculated route
self.route, route_data = result
self.nav_curvature_valid = True
# Start at first step (simple, like navd.py)
self.tracker.set_step(0)
self._reset_recompute_limits() # Reset backoff on successful route
cloudlog.warning(f"maad: route calculated - {route_data['distance']/1000:.1f}km, "
f"{route_data['duration']/60:.0f}min, {len(self.route)} steps")
# Debug: log each step's maneuver info
for i, s in enumerate(self.route):
mp_str = f"({s.maneuver_point.latitude:.6f},{s.maneuver_point.longitude:.6f})" if s.maneuver_point else "None"
cloudlog.info(f"maad step {i}: {s.maneuver_type} {s.maneuver_modifier} -> '{s.name}' ({s.distance:.0f}m) mp={mp_str}")
self.send_route()
except queue.Empty:
pass # No result yet
def _update_location(self):
"""Update position from GPS."""
# Debug: log GPS reception status every 50 frames
self._gps_debug_counter = getattr(self, '_gps_debug_counter', 0) + 1
if self._gps_debug_counter >= 50:
self._gps_debug_counter = 0
gps = self.sm['liveGPS']
cloudlog.info(f"maad GPS: updated={self.sm.updated['liveGPS']} valid={self.sm.valid['liveGPS']} "
f"gpsOK={gps.gpsOK} pos=({gps.latitude:.6f},{gps.longitude:.6f})")
if self.sm.updated['liveGPS']:
gps = self.sm['liveGPS']
# Always update position and speed from GPS (needed for route calculation)
if gps.gpsOK:
self.last_position = Coordinate(gps.latitude, gps.longitude)
self.gps_speed = gps.speed
self.gps_accuracy = gps.horizontalAccuracy # Track for OsmAnd-style tolerance
self.last_gps_time = time.monotonic()
# Save last position every ~60 frames (12 seconds at 5Hz) to reduce I/O
self._gps_save_counter += 1
if self._gps_save_counter >= 60:
self._gps_save_counter = 0
self.params.put("LastGPSPosition", json.dumps({
'latitude': gps.latitude,
'longitude': gps.longitude
}))
# GPS is valid when OK, good accuracy, AND fully calibrated
was_gps_ok = self.gps_ok
is_calibrated = gps.status == custom.LiveGPS.Status.valid
self.gps_ok = gps.gpsOK and gps.horizontalAccuracy <= 20.0 and is_calibrated
# Only use bearing when fusion is fully calibrated
if is_calibrated:
self.last_bearing = gps.bearingDeg
else:
self.last_bearing = None # clear stale bearing
# Detect first valid GPS - triggers immediate route check
if self.gps_ok and not was_gps_ok and not self._had_first_valid_gps:
self._had_first_valid_gps = True
cloudlog.info("maad: first valid GPS fix (calibrated), checking for destination")
# Staleness check
if time.monotonic() - self.last_gps_time > 2.0:
self.gps_ok = False
def _recompute_route(self):
"""Check if we need to recompute route."""
# Don't start route until GPS is valid (OK + calibrated)
if not self.gps_ok:
return
# Skip if route calculation is already in progress
if self._route_calculating:
return
# Check params - immediately on first valid GPS, otherwise every 3 seconds
now = time.monotonic()
first_gps_check = self._had_first_valid_gps and self.route is None and self._cached_destination is None
if first_gps_check or now - self._last_destination_check >= self._destination_check_interval:
self._last_destination_check = now
self._cached_destination = coordinate_from_param("dp_maa_destination", self.params)
new_destination = self._cached_destination
if new_destination is None:
if self.nav_destination is not None or self.route is not None:
self._clear_route()
self._reset_recompute_limits()
return
should_recompute = self._should_recompute()
if should_recompute and self.route is not None:
cloudlog.warning(f"maad: reroute triggered, countdown={self.recompute_countdown}, backoff={self.recompute_backoff}")
# New destination
if new_destination != self.nav_destination:
cloudlog.warning(f"Got new destination from dp_maa_destination param {new_destination}")
self.nav_destination = new_destination
should_recompute = True
# Don't recompute when GPS drifts in tunnels
if not self.gps_ok and self.tracker.step_idx is not None:
return
# First route calculation (no existing route) - start immediately without backoff
is_first_route = self.route is None and should_recompute
if is_first_route or (self.recompute_countdown == 0 and should_recompute):
if not is_first_route:
self.recompute_countdown = 2 ** self.recompute_backoff
self.recompute_backoff = min(3, self.recompute_backoff + 1) # Max 8 second backoff
self._start_route_calculation(new_destination)
else:
self.recompute_countdown = max(0, self.recompute_countdown - 1)
def _start_route_calculation(self, destination: Coordinate):
"""Start async route calculation in a separate thread."""
start_pos = self.last_position
bearing = self.last_bearing
self._route_calculating = True
self.nav_destination = destination
cloudlog.info(f"maad: starting async route calculation {start_pos} -> {destination}")
def calculate():
try:
result = self._fetch_route(start_pos, destination, bearing)
self._route_queue.put(result)
except Exception as e:
cloudlog.exception(f"maad: route calculation failed: {e}")
self._route_queue.put(None)
self._route_thread = threading.Thread(target=calculate, daemon=True)
self._route_thread.start()
def _fetch_route(self, start: Coordinate, destination: Coordinate,
bearing: Optional[float]) -> Optional[tuple]:
"""Fetch route using MapService (runs in thread). Returns (route, route_data) or None."""
origin = ProviderCoordinate(start.latitude, start.longitude)
dest = ProviderCoordinate(destination.latitude, destination.longitude)
provider_route = self.map_service.route_provider.get_route_sync(
origin=origin,
destination=dest,
bearing=bearing
)
if provider_route is None:
cloudlog.warning("maad: route provider returned None")
return None
# Convert provider Route to local Step format
# Filter out depart/arrive - merge their geometry with adjacent steps
route = []
all_coords = [] # Full route geometry for turn angle computation
pending_geometry = [] # Geometry from depart to merge with first real step
for provider_step in provider_route.steps:
# Convert provider Coordinates to helper Coordinates
geometry = [
Coordinate(c.latitude, c.longitude)
for c in provider_step.geometry
]
all_coords.extend(geometry)
# Skip depart/arrive steps but keep their geometry
if provider_step.maneuver_type in ('depart', 'arrive'):
if provider_step.maneuver_type == 'depart':
pending_geometry = geometry # Save for merging with first real step
elif provider_step.maneuver_type == 'arrive' and route:
# Merge arrive geometry with last step
route[-1].geometry.extend(geometry)
route[-1].distance += provider_step.distance
route[-1].duration += provider_step.duration
continue
# Merge pending depart geometry with this step
if pending_geometry:
geometry = pending_geometry + geometry
pending_geometry = []
# Convert provider maneuver_point to helpers Coordinate
maneuver_pt = None
if provider_step.maneuver_point:
maneuver_pt = Coordinate(
provider_step.maneuver_point.latitude,
provider_step.maneuver_point.longitude
)
route_step = Step(
distance=provider_step.distance,
duration=provider_step.duration,
duration_typical=provider_step.duration_typical or provider_step.duration,
name=provider_step.name,
maneuver_type=provider_step.maneuver_type,
maneuver_modifier=provider_step.maneuver_modifier,
geometry=geometry,
speed_limit=provider_step.speed_limit,
speed_limit_sign=provider_step.speed_limit_sign,
maneuver_point=maneuver_pt,
)
route.append(route_step)
# Pre-compute turn geometry at each step's maneuver point (end of step)
coord_idx = 0
for step in route:
coord_idx += len(step.geometry)
# Turn point is at the end of this step (start of next)
turn_idx = min(coord_idx - 1, len(all_coords) - 2)
if turn_idx > 0:
step.turn_angle = compute_turn_angle_at_index(all_coords, turn_idx)
step.turn_curvature = compute_turn_curvature_at_index(all_coords, turn_idx)
# Build route_data dict for compatibility
route_data = {
'distance': provider_route.distance,
'duration': provider_route.duration,
}
cloudlog.info(f"maad: route from {provider_route.provider} - "
f"{provider_route.distance/1000:.1f}km, {len(route)} steps")
return (route, route_data)
def _send_instruction(self):
"""Send navInstruction message ."""
msg = messaging.new_message('navInstruction', valid=True)
if self.tracker.step_idx is None or self.route is None or self.last_position is None or not self.gps_ok:
# Debug: log why we're sending invalid
reasons = []
if self.tracker.step_idx is None:
reasons.append("step_idx=None")
if self.route is None:
reasons.append("route=None")
if self.last_position is None:
reasons.append("position=None")
if not self.gps_ok:
reasons.append(f"gps_ok=False")
cloudlog.info(f"maad: sending invalid navInstruction: {', '.join(reasons)}")
msg.valid = False
self.pm.send('navInstruction', msg)
return
# Sanity check: ensure step_idx is valid
if self.tracker.step_idx >= len(self.route):
cloudlog.error(f"maad: step_idx {self.tracker.step_idx} >= route length {len(self.route)}, resetting to 0")
self.tracker.set_step(0)
step = self.route[self.tracker.step_idx]
geometry = step.geometry
# Calculate distance along current step geometry
along_geometry = distance_along_geometry(geometry, self.last_position)
distance_to_maneuver = step.distance - along_geometry
# Current instruction (depart/arrive already filtered out during route build)
msg.navInstruction.maneuverDistance = distance_to_maneuver
msg.navInstruction.maneuverPrimaryText = step.name or step.maneuver_type
# Override maneuver type/modifier based on geometry
# Geometry-first: always use turn_angle for direction when significant
TURN_MIN_ANGLE = 20.0
if abs(step.turn_angle) >= TURN_MIN_ANGLE:
# Significant turn - use geometry for both type and direction
if step.maneuver_type in ('continue', 'new name'):
msg.navInstruction.maneuverType = 'turn'
else:
msg.navInstruction.maneuverType = step.maneuver_type
# Always use geometry-based direction for significant turns
msg.navInstruction.maneuverModifier = 'left' if step.turn_angle > 0 else 'right'
else:
msg.navInstruction.maneuverType = step.maneuver_type
msg.navInstruction.maneuverModifier = step.maneuver_modifier
# Next step's road name (the road to turn onto)
if self.tracker.step_idx + 1 < len(self.route):
next_step = self.route[self.tracker.step_idx + 1]
msg.navInstruction.maneuverSecondaryText = next_step.name or ""
# Compute total remaining time and distance
remaining_ratio = 1.0 - along_geometry / max(step.distance, 1)
total_distance = step.distance * remaining_ratio
total_time = step.duration * remaining_ratio
total_time_typical = (step.duration_typical or step.duration) * remaining_ratio
for i in range(self.tracker.step_idx + 1, len(self.route)):
total_distance += self.route[i].distance
total_time += self.route[i].duration
total_time_typical += self.route[i].duration_typical or self.route[i].duration
msg.navInstruction.distanceRemaining = total_distance
msg.navInstruction.timeRemaining = total_time
msg.navInstruction.timeRemainingTypical = total_time_typical
# Speed limit from closest coordinate
if geometry:
closest_idx, _ = find_closest_point_on_route(self.last_position, geometry)
if closest_idx < len(geometry):
closest = geometry[closest_idx]
if 'maxspeed' in closest.annotations and self.gps_ok:
msg.navInstruction.speedLimit = closest.annotations['maxspeed']
if step.speed_limit_sign == 'mutcd':
msg.navInstruction.speedLimitSign = log.NavInstruction.SpeedLimitSign.mutcd
else:
msg.navInstruction.speedLimitSign = log.NavInstruction.SpeedLimitSign.vienna
self.pm.send('navInstruction', msg)
# Send extended nav instruction (turn geometry)
msg_ext = messaging.new_message('navInstructionExt')
msg_ext.navInstructionExt.turnAngle = step.turn_angle
msg_ext.navInstructionExt.turnCurvature = step.turn_curvature
self.pm.send('navInstructionExt', msg_ext)
# Transition to next step
if self.tracker.update_step(self.route, self.last_position, self.last_bearing, self.gps_speed):
self._reset_recompute_limits()
# Check if arrived at destination
if self.nav_destination:
dist = self.nav_destination.distance_to(self.last_position)
if dist < 30: # Within 30m of destination
cloudlog.warning("maad: destination reached")
self.params.remove("dp_maa_destination")
self._clear_route()
def send_route(self):
"""Send navRoute message for dashy to display route on map."""
coords = []
if self.route is not None:
for step in self.route:
coords.extend([[c.longitude, c.latitude] for c in step.geometry])
msg = messaging.new_message('navRoute', valid=True)
msg.navRoute.coordinates = [{"longitude": c[0], "latitude": c[1]} for c in coords]
self.pm.send('navRoute', msg)
def _clear_route(self, clear_destination=True):
"""Clear navigation state."""
self.route = None
self.tracker.reset()
if clear_destination:
self.nav_destination = None
# Send empty navRoute to clear map display
msg = messaging.new_message('navRoute', valid=False)
msg.navRoute.coordinates = []
self.pm.send('navRoute', msg)
def _reset_recompute_limits(self):
"""Reset recompute backoff and deviation timer."""
self.recompute_backoff = 0
self.recompute_countdown = 0
self.tracker.deviation_start_time = None # Reset OsmAnd-style deviation timer
def _should_recompute(self) -> bool:
"""Check if route should be recomputed (delegates to RouteTracker)."""
if self.route is None:
return True
return self.tracker.should_reroute(self.route, self.last_position, self.gps_accuracy)
def main():
cloudlog.info("maad starting")
pm = messaging.PubMaster(['navInstruction', 'navInstructionExt', 'navRoute'])
sm = messaging.SubMaster(['liveGPS'], ignore_alive=['liveGPS'])
rk = Ratekeeper(NAV_RATE)
route_engine = RouteEngine(sm, pm)
while True:
try:
route_engine.update()
except Exception:
cloudlog.exception("maad: error in main loop")
rk.keep_time()
if __name__ == "__main__":
main()
@@ -0,0 +1,54 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Map Provider Abstraction Layer
Uses api.dragonpilot.org for search and routing with device JWT authentication.
Falls back to free providers (Photon/OSRM) when not authenticated.
Usage:
from dragonpilot.dashy.maa.providers import MapService
map_service = MapService()
route = map_service.route_provider.get_route_sync(origin, dest)
results = await map_service.search_provider.search("Taipei 101")
"""
from .map_service import MapService
from .models import Coordinate, SearchResult, Route, Step, TileConfig
from .base import SearchProvider, RouteProvider, TileProvider
from .dragonpilot import DragonpilotSearchProvider, DragonpilotRouteProvider, DragonpilotApiClient
__all__ = [
'MapService',
'Coordinate',
'SearchResult',
'Route',
'Step',
'TileConfig',
'SearchProvider',
'RouteProvider',
'TileProvider',
'DragonpilotSearchProvider',
'DragonpilotRouteProvider',
'DragonpilotApiClient',
]
+177
View File
@@ -0,0 +1,177 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Abstract base classes for map providers.
These interfaces define the contract that all provider implementations must follow.
"""
from abc import ABC, abstractmethod
from typing import Optional
import asyncio
from .models import Coordinate, SearchResult, Route, TileConfig
class SearchProvider(ABC):
"""
Abstract search/geocoding provider.
Implementations: PhotonSearchProvider, MapboxSearchProvider, etc.
"""
name: str = "base"
requires_api_key: bool = False
@abstractmethod
async def search(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""
Search for places by query string.
Args:
query: Search query (address, place name, etc.)
proximity: Optional coordinate to bias results toward
limit: Maximum number of results
Returns:
List of SearchResult objects sorted by relevance/distance
"""
pass
@abstractmethod
async def reverse_geocode(
self,
coord: Coordinate
) -> Optional[SearchResult]:
"""
Get address/place information from coordinates.
Args:
coord: Coordinate to reverse geocode
Returns:
SearchResult with address info, or None if not found
"""
pass
def search_sync(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Synchronous wrapper for search()."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(self.search(query, proximity, limit))
finally:
loop.close()
class RouteProvider(ABC):
"""
Abstract routing provider.
Implementations: OSRMRouteProvider, MapboxRouteProvider, etc.
"""
name: str = "base"
requires_api_key: bool = False
supports_traffic: bool = False
@abstractmethod
async def get_route(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""
Calculate route between points.
Args:
origin: Starting coordinate
destination: Ending coordinate
waypoints: Optional intermediate waypoints
bearing: Optional current heading in degrees (for better route start)
Returns:
Route object with steps and geometry, or None if routing fails
"""
pass
def get_route_sync(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""
Synchronous wrapper for get_route().
Use this in maad.py where async is not available.
"""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(
self.get_route(origin, destination, waypoints, bearing)
)
finally:
loop.close()
class TileProvider(ABC):
"""
Abstract map tile provider.
Implementations: OpenFreeMapTileProvider, MapboxTileProvider, etc.
"""
name: str = "base"
requires_api_key: bool = False
@abstractmethod
def get_tile_config(self) -> TileConfig:
"""
Get tile URL template and configuration.
Returns:
TileConfig with URL template and attribution
"""
pass
@abstractmethod
def get_style_json(self) -> dict:
"""
Get MapLibre GL style JSON for this provider.
Returns:
Style JSON dict for MapLibre GL JS
"""
pass
+45
View File
@@ -0,0 +1,45 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Configuration for map providers.
Uses api.dragonpilot.org for search and routing with device serial authentication.
Falls back to free providers (Photon/OSRM) when not authenticated.
All providers use WGS-84 coordinates (standard GPS).
"""
from dataclasses import dataclass
@dataclass
class ProviderConfig:
"""
Provider configuration.
Uses Dragonpilot API with automatic fallback to free providers.
All providers use WGS-84 coordinates (standard GPS).
"""
@classmethod
def from_params(cls, params) -> 'ProviderConfig':
"""Load configuration from openpilot Params."""
return cls()
@@ -0,0 +1,32 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot API Provider
Uses api.dragonpilot.org for search and routing with device JWT authentication.
"""
from .client import DragonpilotApiClient
from .search import DragonpilotSearchProvider
from .routing import DragonpilotRouteProvider
__all__ = ['DragonpilotApiClient', 'DragonpilotSearchProvider', 'DragonpilotRouteProvider']
@@ -0,0 +1,123 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot API Client
Simple HTTP client using device serial for authentication.
"""
import os
from typing import Optional
import aiohttp
import requests
from openpilot.system.hardware import HARDWARE
API_HOST = os.getenv('DRAGONPILOT_API_HOST', 'https://api.dragonpilot.org')
# Module-level serial cache - queried once from HARDWARE
_serial: Optional[str] = None
def _get_serial() -> Optional[str]:
"""Get device serial (cached)."""
global _serial
if _serial is None:
try:
_serial = HARDWARE.get_serial()
except Exception:
pass
return _serial
class DragonpilotApiClient:
"""
API client for api.dragonpilot.org.
Uses device serial from HARDWARE for authentication.
"""
def __init__(self, serial: str = None):
self.api_host = API_HOST
self.serial = serial if serial is not None else _get_serial()
@property
def is_authenticated(self) -> bool:
return self.serial is not None
def _headers(self) -> dict:
headers = {'Content-Type': 'application/json'}
if self.serial:
headers['X-Device-Serial'] = self.serial
return headers
def get_sync(self, endpoint: str, params: dict = None, timeout: int = 10) -> Optional[dict]:
try:
resp = requests.get(f"{self.api_host}{endpoint}", params=params, headers=self._headers(), timeout=timeout)
return resp.json() if resp.status_code == 200 else None
except Exception:
return None
def post_sync(self, endpoint: str, data: dict = None, timeout: int = 10) -> Optional[dict]:
try:
resp = requests.post(f"{self.api_host}{endpoint}", json=data, headers=self._headers(), timeout=timeout)
return resp.json() if resp.status_code == 200 else None
except Exception:
return None
async def get(self, endpoint: str, params: dict = None, timeout: int = 10) -> Optional[dict]:
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.api_host}{endpoint}",
params=params,
headers=self._headers(),
timeout=aiohttp.ClientTimeout(total=timeout)
) as resp:
return await resp.json() if resp.status == 200 else None
except Exception:
return None
async def post(self, endpoint: str, data: dict = None, timeout: int = 10) -> Optional[dict]:
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.api_host}{endpoint}",
json=data,
headers=self._headers(),
timeout=aiohttp.ClientTimeout(total=timeout)
) as resp:
return await resp.json() if resp.status == 200 else None
except Exception:
return None
# Singleton client instance
_client: Optional[DragonpilotApiClient] = None
def get_client() -> DragonpilotApiClient:
"""Get the shared API client instance."""
global _client
if _client is None:
_client = DragonpilotApiClient()
return _client
@@ -0,0 +1,269 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot Routing Provider
Uses api.dragonpilot.org routing endpoint with device serial authentication.
Falls back to OSRM if not authenticated or on error.
"""
from typing import Optional
from openpilot.common.swaglog import cloudlog
from ..base import RouteProvider
from ..models import Coordinate, Route, Step
from .client import get_client
def decode_polyline(polyline: str) -> list[tuple[float, float]]:
"""
Decode a Google/HERE Encoded Polyline into (lat, lon) tuples.
Algorithm: https://developers.google.com/maps/documentation/utilities/polylinealgorithm
"""
coordinates = []
index = 0
lat = 0
lng = 0
while index < len(polyline):
# Decode latitude
result = 0
shift = 0
while True:
b = ord(polyline[index]) - 63
index += 1
result |= (b & 0x1f) << shift
shift += 5
if b < 0x20:
break
lat += (~(result >> 1) if result & 1 else result >> 1)
# Decode longitude
result = 0
shift = 0
while True:
b = ord(polyline[index]) - 63
index += 1
result |= (b & 0x1f) << shift
shift += 5
if b < 0x20:
break
lng += (~(result >> 1) if result & 1 else result >> 1)
coordinates.append((lat / 1e5, lng / 1e5))
return coordinates
# Map action to OSRM-style maneuver type/modifier
ACTION_MAP = {
'depart': ('depart', ''),
'arrive': ('arrive', ''),
'turn': ('turn', ''),
'turn-left': ('turn', 'left'),
'turn-right': ('turn', 'right'),
'turn-slight-left': ('turn', 'slight left'),
'turn-slight-right': ('turn', 'slight right'),
'turn-sharp-left': ('turn', 'sharp left'),
'turn-sharp-right': ('turn', 'sharp right'),
'continue': ('continue', 'straight'),
'keep': ('continue', ''),
'merge': ('merge', ''),
'roundabout': ('roundabout', ''),
'roundaboutExit': ('roundabout', 'exit'),
'ferry': ('ferry', ''),
'uturn': ('turn', 'uturn'),
}
class DragonpilotRouteProvider(RouteProvider):
"""
Dragonpilot API routing provider.
Uses device serial for authentication. Falls back to OSRM on auth failure.
API Response format:
{
"route": {
"route_id": "...",
"distance_m": 6837,
"duration_s": 759,
"duration_traffic_s": 1595,
"polyline": "...",
"maneuvers": [{
"instruction": "Turn left onto Main Street",
"distance_m": 500,
"duration_s": 60,
"position": {"lat": 25.03, "lon": 121.56},
"action": "turn"
}],
"provider": "here"
},
"cached": false,
"provider": "here"
}
"""
name = "dragonpilot"
requires_api_key = False
supports_traffic = True
def __init__(self):
self._client = get_client()
self._fallback = None
def _get_fallback(self) -> RouteProvider:
"""Get fallback OSRM provider."""
if self._fallback is None:
from ..routing.osrm import OSRMRouteProvider
self._fallback = OSRMRouteProvider()
return self._fallback
async def get_route(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using Dragonpilot API (async)."""
if not self._client.is_authenticated:
cloudlog.warning("dragonpilot routing: no serial, using osrm fallback")
return await self._get_fallback().get_route(origin, destination, waypoints, bearing)
data = self._build_request(origin, destination, waypoints)
response = await self._client.post('/v1/route', data=data, timeout=30)
if response is None:
cloudlog.warning("dragonpilot routing: API error, using osrm fallback")
return await self._get_fallback().get_route(origin, destination, waypoints, bearing)
return self._parse_response(response)
def get_route_sync(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using Dragonpilot API (synchronous)."""
if not self._client.is_authenticated:
cloudlog.warning("dragonpilot routing: no serial, using osrm fallback")
return self._get_fallback().get_route_sync(origin, destination, waypoints, bearing)
data = self._build_request(origin, destination, waypoints)
response = self._client.post_sync('/v1/route', data=data, timeout=30)
if response is None:
cloudlog.warning("dragonpilot routing: API error, using osrm fallback")
return self._get_fallback().get_route_sync(origin, destination, waypoints, bearing)
return self._parse_response(response)
def _build_request(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]]
) -> dict:
"""Build API request body."""
origin = origin.to_wgs84() if hasattr(origin, 'to_wgs84') else origin
destination = destination.to_wgs84() if hasattr(destination, 'to_wgs84') else destination
data = {
'origin': {'lat': origin.latitude, 'lon': origin.longitude},
'destination': {'lat': destination.latitude, 'lon': destination.longitude},
}
if waypoints:
data['waypoints'] = []
for wp in waypoints:
wp = wp.to_wgs84() if hasattr(wp, 'to_wgs84') else wp
data['waypoints'].append({'lat': wp.latitude, 'lon': wp.longitude})
return data
def _parse_response(self, data: dict) -> Optional[Route]:
"""Parse API response into Route object."""
if not data:
return None
# Unwrap route object
route_data = data.get('route', data)
# Decode full route geometry
full_geometry = []
polyline = route_data.get('polyline', '')
if polyline:
try:
for lat, lon in decode_polyline(polyline):
coord = Coordinate(lat, lon)
full_geometry.append(coord)
except Exception:
pass
# Parse maneuvers into steps
steps = []
maneuvers = route_data.get('maneuvers', [])
for maneuver in maneuvers:
pos = maneuver.get('position', {})
action = maneuver.get('action', 'continue')
# Map action to maneuver type/modifier
maneuver_type, maneuver_modifier = ACTION_MAP.get(action, ('continue', ''))
# Get maneuver point
maneuver_point = None
if pos.get('lat') is not None and pos.get('lon') is not None:
maneuver_point = Coordinate(pos['lat'], pos['lon'])
# Use full instruction text as the step name
instruction = maneuver.get('instruction', '')
steps.append(Step(
distance=maneuver.get('distance_m', 0),
duration=maneuver.get('duration_s', 0),
name=instruction,
maneuver_type=maneuver_type,
maneuver_modifier=maneuver_modifier,
geometry=[maneuver_point] if maneuver_point else [],
speed_limit=None,
speed_limit_sign='vienna',
maneuver_point=maneuver_point,
))
# Use traffic duration if available
duration = route_data.get('duration_traffic_s') or route_data.get('duration_s', 0)
return Route(
steps=steps,
distance=route_data.get('distance_m', 0),
duration=duration,
geometry=full_geometry,
provider=self.name,
has_traffic=route_data.get('duration_traffic_s') is not None,
raw=data,
)
@@ -0,0 +1,191 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot Search Provider
Uses api.dragonpilot.org geocoding endpoint for address search.
Falls back to Photon if not authenticated or on error.
"""
from typing import Optional
from openpilot.common.swaglog import cloudlog
from ..base import SearchProvider
from ..models import Coordinate, SearchResult
from .client import get_client
class DragonpilotSearchProvider(SearchProvider):
"""
Dragonpilot API search provider.
Uses device serial for authentication. Falls back to Photon on auth failure.
API Response format:
{
"results": [{
"id": "here:...",
"title": "Taipei 101",
"address": "No. 7, Section 5, Xinyi Road",
"position": {"lat": 25.033, "lon": 121.565},
"category": "landmark",
"distance_m": 150
}],
"provider": "here"
}
"""
name = "dragonpilot"
requires_api_key = False
def __init__(self):
self._client = get_client()
self._fallback = None
def _get_fallback(self) -> SearchProvider:
"""Get fallback Photon provider."""
if self._fallback is None:
from ..search.photon import PhotonSearchProvider
self._fallback = PhotonSearchProvider()
return self._fallback
async def search(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Dragonpilot API (async)."""
if not query or len(query) < 1:
return []
# Fall back to Photon if not authenticated
if not self._client.is_authenticated:
cloudlog.debug("dragonpilot search: no serial, using photon fallback")
return await self._get_fallback().search(query, proximity, limit)
params = {'q': query, 'limit': min(limit, 10)}
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
data = await self._client.get('/v1/geocode/autocomplete', params=params)
if data is None:
cloudlog.debug("dragonpilot search: API error, using photon fallback")
return await self._get_fallback().search(query, proximity, limit)
return self._parse_results(data, proximity, limit)
def search_sync(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Dragonpilot API (synchronous)."""
if not query or len(query) < 1:
return []
# Fall back to Photon if not authenticated
if not self._client.is_authenticated:
cloudlog.debug("dragonpilot search: no serial, using photon fallback")
return self._get_fallback().search_sync(query, proximity, limit)
params = {'q': query, 'limit': min(limit, 10)}
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
data = self._client.get_sync('/v1/geocode/autocomplete', params=params)
if data is None:
cloudlog.debug("dragonpilot search: API error, using photon fallback")
return self._get_fallback().search_sync(query, proximity, limit)
return self._parse_results(data, proximity, limit)
async def reverse_geocode(self, coord: Coordinate) -> Optional[SearchResult]:
"""Get address from coordinates. Falls back to Photon."""
return await self._get_fallback().reverse_geocode(coord)
def _parse_results(
self,
data: dict,
proximity: Optional[Coordinate],
limit: int
) -> list[SearchResult]:
"""Parse API response into SearchResult list."""
results = []
# Handle both GeoJSON format (features) and simple format (results)
items = data.get('features', data.get('results', []))
for item in items:
try:
# GeoJSON format
if 'geometry' in item:
coords = item['geometry']['coordinates']
lon, lat = coords[0], coords[1] # GeoJSON is [lon, lat]
props = item.get('properties', {})
title = props.get('title', 'Unknown')
address = props.get('address', '')
place_id = props.get('id')
distance = props.get('distance_m')
# Simple format
else:
pos = item.get('position', {})
lat = pos.get('lat')
lon = pos.get('lon')
title = item.get('title', 'Unknown')
address = item.get('address', '')
place_id = item.get('id')
distance = item.get('distance_m')
if lat is None or lon is None:
continue
coord = Coordinate(lat, lon)
# Calculate distance if not provided
if distance is None and proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
distance = Coordinate(lat, lon).distance_to(prox)
results.append(SearchResult(
name=title,
address=address,
coordinate=coord,
distance=distance,
place_id=place_id,
provider=self.name,
raw=item,
))
except (KeyError, TypeError, IndexError):
continue
return results[:limit]
@@ -0,0 +1,56 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Provider factory for creating map service instances.
Uses api.dragonpilot.org for search and routing.
"""
from .config import ProviderConfig
from .base import SearchProvider, RouteProvider, TileProvider
class ProviderFactory:
"""
Factory for creating map providers.
Uses Dragonpilot API providers which have built-in fallback
to free providers (Photon/OSRM) when not authenticated.
"""
@classmethod
def create_search_provider(cls, config: ProviderConfig) -> SearchProvider:
"""Create search provider."""
from .dragonpilot.search import DragonpilotSearchProvider
return DragonpilotSearchProvider()
@classmethod
def create_route_provider(cls, config: ProviderConfig) -> RouteProvider:
"""Create route provider."""
from .dragonpilot.routing import DragonpilotRouteProvider
return DragonpilotRouteProvider()
@classmethod
def create_tile_provider(cls, config: ProviderConfig) -> TileProvider:
"""Create tile provider."""
from .tiles.openfreemap import OpenFreeMapTileProvider
return OpenFreeMapTileProvider()
@@ -0,0 +1,188 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Map Service - Main entry point for all map operations.
Usage:
from dragonpilot.dashy.maa.providers import MapService
# In maad.py (sync):
map_service = MapService()
route = map_service.route_provider.get_route_sync(origin, dest)
# In server.py (async):
results = await MapService.get_instance().search_provider.search("Taipei 101")
# Get tile config for frontend:
tile_config = map_service.get_tile_config_for_js()
"""
from typing import Optional
from .config import ProviderConfig
from .factory import ProviderFactory
from .base import SearchProvider, RouteProvider, TileProvider
class MapService:
"""
Main entry point for all map operations.
Provides lazy-loaded access to search, routing, and tile providers.
Configuration is read from openpilot Params.
"""
_instance: Optional['MapService'] = None
def __init__(self, params=None):
"""
Initialize MapService.
Args:
params: Optional openpilot Params instance.
If None, will be created when needed.
"""
self._params = params
self._config: Optional[ProviderConfig] = None
self._search_provider: Optional[SearchProvider] = None
self._route_provider: Optional[RouteProvider] = None
self._tile_provider: Optional[TileProvider] = None
@classmethod
def get_instance(cls, params=None) -> 'MapService':
"""
Get singleton instance of MapService.
Args:
params: Optional Params instance for first-time initialization
"""
if cls._instance is None:
cls._instance = cls(params)
return cls._instance
@classmethod
def reset_instance(cls):
"""Reset singleton instance. Useful for testing or config reload."""
cls._instance = None
def _get_params(self):
"""Get or create Params instance."""
if self._params is None:
from openpilot.common.params import Params
self._params = Params()
return self._params
def reload_config(self):
"""
Reload configuration from params and recreate providers.
Call this after changing provider settings in params.
"""
self._config = ProviderConfig.from_params(self._get_params())
self._search_provider = None
self._route_provider = None
self._tile_provider = None
@property
def config(self) -> ProviderConfig:
"""Get current configuration, loading from params if needed."""
if self._config is None:
self._config = ProviderConfig.from_params(self._get_params())
return self._config
@property
def search_provider(self) -> SearchProvider:
"""Get search provider, creating if needed."""
if self._search_provider is None:
# Import providers to trigger registration
self._ensure_providers_imported()
self._search_provider = ProviderFactory.create_search_provider(self.config)
return self._search_provider
@property
def route_provider(self) -> RouteProvider:
"""Get route provider, creating if needed."""
if self._route_provider is None:
# Import providers to trigger registration
self._ensure_providers_imported()
self._route_provider = ProviderFactory.create_route_provider(self.config)
return self._route_provider
@property
def tile_provider(self) -> TileProvider:
"""Get tile provider, creating if needed."""
if self._tile_provider is None:
# Import providers to trigger registration
self._ensure_providers_imported()
self._tile_provider = ProviderFactory.create_tile_provider(self.config)
return self._tile_provider
def _ensure_providers_imported(self):
"""Import provider modules to trigger registration."""
try:
from . import search
from . import routing
from . import tiles
except ImportError:
pass
def get_tile_config_for_js(self) -> dict:
"""
Get tile configuration as dict for JavaScript frontend.
Returns dict suitable for sending to frontend via API.
"""
tile_config = self.tile_provider.get_tile_config()
style = self.tile_provider.get_style_json()
return {
'provider': self.config.tile_provider.value,
'url_template': tile_config.url_template,
'style': style,
'attribution': tile_config.attribution,
'min_zoom': tile_config.min_zoom,
'max_zoom': tile_config.max_zoom,
}
def get_provider_info(self) -> dict:
"""
Get information about current providers.
Useful for debugging and UI display.
"""
return {
'search': {
'provider': self.config.search_provider.value,
'name': self.search_provider.name,
'requires_api_key': self.search_provider.requires_api_key,
},
'route': {
'provider': self.config.route_provider.value,
'name': self.route_provider.name,
'requires_api_key': self.route_provider.requires_api_key,
'supports_traffic': self.route_provider.supports_traffic,
},
'tile': {
'provider': self.config.tile_provider.value,
'name': self.tile_provider.name,
'requires_api_key': self.tile_provider.requires_api_key,
},
}
+180
View File
@@ -0,0 +1,180 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Data models for map providers.
These models provide a common interface for search results, routes, and coordinates
across different providers (OSRM, Mapbox, Google, AMap).
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
EARTH_MEAN_RADIUS = 6371007.2 # meters
class CoordinateSystem(Enum):
"""Coordinate reference system."""
WGS84 = "wgs84" # Standard GPS coordinates (used globally)
GCJ02 = "gcj02" # China encrypted coordinates (required for China maps)
@dataclass
class Coordinate:
"""
Geographic coordinate with optional coordinate system awareness.
Supports WGS-84 (standard GPS) and GCJ-02 (China) coordinate systems.
Provides distance and bearing calculations.
"""
latitude: float
longitude: float
system: CoordinateSystem = CoordinateSystem.WGS84
annotations: dict = field(default_factory=dict)
def distance_to(self, other: Coordinate) -> float:
"""Calculate Haversine distance to another coordinate in meters."""
dlat = math.radians(other.latitude - self.latitude)
dlon = math.radians(other.longitude - self.longitude)
haversine_dlat = math.sin(dlat / 2.0)
haversine_dlat *= haversine_dlat
haversine_dlon = math.sin(dlon / 2.0)
haversine_dlon *= haversine_dlon
y = haversine_dlat + \
math.cos(math.radians(self.latitude)) * \
math.cos(math.radians(other.latitude)) * \
haversine_dlon
x = 2 * math.asin(math.sqrt(y))
return x * EARTH_MEAN_RADIUS
def bearing_to(self, other: Coordinate) -> float:
"""Calculate bearing to another coordinate in degrees (0-360)."""
lat1, lat2 = math.radians(self.latitude), math.radians(other.latitude)
dlon = math.radians(other.longitude - self.longitude)
x = math.sin(dlon) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
bearing = math.degrees(math.atan2(x, y))
return (bearing + 360) % 360
def to_wgs84(self) -> Coordinate:
"""Convert to WGS-84 coordinate system."""
if self.system == CoordinateSystem.WGS84:
return self
from .utils.coordinates import gcj02_to_wgs84
lat, lon = gcj02_to_wgs84(self.latitude, self.longitude)
return Coordinate(lat, lon, CoordinateSystem.WGS84, self.annotations.copy())
def to_gcj02(self) -> Coordinate:
"""Convert to GCJ-02 coordinate system (for China maps)."""
if self.system == CoordinateSystem.GCJ02:
return self
from .utils.coordinates import wgs84_to_gcj02
lat, lon = wgs84_to_gcj02(self.latitude, self.longitude)
return Coordinate(lat, lon, CoordinateSystem.GCJ02, self.annotations.copy())
def as_dict(self) -> dict:
"""Convert to dictionary."""
return {'latitude': self.latitude, 'longitude': self.longitude}
def __str__(self) -> str:
return f'Coordinate({self.latitude:.6f}, {self.longitude:.6f})'
def __repr__(self) -> str:
return self.__str__()
def __eq__(self, other) -> bool:
if not isinstance(other, Coordinate):
return False
return self.latitude == other.latitude and self.longitude == other.longitude
def __hash__(self) -> int:
return hash((self.latitude, self.longitude))
@dataclass
class SearchResult:
"""
Normalized search/geocoding result.
All search providers return results in this format.
"""
name: str # Display name (e.g., "Taipei 101")
address: str # Full address string
coordinate: Coordinate # Location
distance: Optional[float] = None # Distance from search proximity (meters)
place_id: Optional[str] = None # Provider-specific place ID
provider: str = "" # Provider name (e.g., "photon", "mapbox")
raw: dict = field(default_factory=dict) # Raw provider response for debugging
@dataclass
class Step:
"""
Single navigation step/maneuver in a route.
Represents one turn or road segment with its geometry.
"""
distance: float # Step distance in meters
duration: float # Step duration in seconds
duration_typical: Optional[float] = None # Typical duration (with traffic)
name: str = "" # Road/street name
maneuver_type: str = "" # Type: turn, fork, off ramp, merge, etc.
maneuver_modifier: str = "" # Direction: left, right, slight left, etc.
geometry: list[Coordinate] = field(default_factory=list) # Path coordinates
speed_limit: Optional[float] = None # Speed limit in m/s
speed_limit_sign: str = "vienna" # Sign type: vienna or mutcd
maneuver_point: Optional[Coordinate] = None # Explicit maneuver location from OSRM
@dataclass
class Route:
"""
Complete navigation route.
Contains all steps from origin to destination with total distance/duration.
"""
steps: list[Step] # List of navigation steps
distance: float # Total distance in meters
duration: float # Total duration in seconds
duration_typical: Optional[float] = None # Typical duration (with traffic)
geometry: list[Coordinate] = field(default_factory=list) # Full route polyline
provider: str = "" # Provider name
has_traffic: bool = False # Whether duration includes traffic
raw: dict = field(default_factory=dict) # Raw provider response
@dataclass
class TileConfig:
"""
Map tile configuration for frontend display.
"""
url_template: str # URL template with {z}/{x}/{y}
style_url: Optional[str] = None # MapLibre style URL
attribution: str = "" # Map attribution text
min_zoom: int = 0
max_zoom: int = 22
@@ -0,0 +1,5 @@
"""Routing providers."""
from .osrm import OSRMRouteProvider
__all__ = ['OSRMRouteProvider']
@@ -0,0 +1,190 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
OSRM Route Provider.
Uses the free OSRM (Open Source Routing Machine) API for routing.
No API key required.
OSRM API Docs: http://project-osrm.org/docs/v5.24.0/api/
"""
from typing import Optional
import aiohttp
import requests
from ..base import RouteProvider
from ..models import Coordinate, Route, Step
OSRM_URL = 'https://router.project-osrm.org'
class OSRMRouteProvider(RouteProvider):
"""
Free OSRM routing provider.
No API key required. Uses public OSRM demo server.
Does not support traffic data.
"""
name = "osrm"
requires_api_key = False
supports_traffic = False
def __init__(self):
"""Initialize OSRM provider."""
pass
async def get_route(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using OSRM API (async)."""
url, params = self._build_request(origin, destination, waypoints, bearing)
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status != 200:
return None
data = await resp.json()
except Exception:
return None
return self._parse_response(data)
def get_route_sync(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using OSRM API (synchronous for maad.py)."""
url, params = self._build_request(origin, destination, waypoints, bearing)
try:
resp = requests.get(url, params=params, timeout=10)
if resp.status_code != 200:
return None
data = resp.json()
if data.get('code') != 'Ok':
return None
except Exception:
return None
return self._parse_response(data)
def _build_request(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]],
bearing: Optional[float]
) -> tuple[str, dict]:
"""Build OSRM API request URL and params."""
# Convert to WGS-84 if needed (OSRM uses WGS-84)
origin = origin.to_wgs84() if hasattr(origin, 'to_wgs84') and origin.system.value != 'wgs84' else origin
destination = destination.to_wgs84() if hasattr(destination, 'to_wgs84') and destination.system.value != 'wgs84' else destination
# Build coordinate string: lon,lat;lon,lat;...
all_coords = [(origin.longitude, origin.latitude)]
if waypoints:
for wp in waypoints:
wp = wp.to_wgs84() if hasattr(wp, 'to_wgs84') and wp.system.value != 'wgs84' else wp
all_coords.append((wp.longitude, wp.latitude))
all_coords.append((destination.longitude, destination.latitude))
# Limit coordinate precision to 6 decimal places (about 0.1m accuracy)
coords_str = ';'.join([f'{lon:.6f},{lat:.6f}' for lon, lat in all_coords])
url = f"{OSRM_URL}/route/v1/driving/{coords_str}"
params = {
'overview': 'full',
'geometries': 'geojson',
'steps': 'true',
}
# Add bearing if provided (helps with route direction at start)
# OSRM bearings format: bearing,range for each coord, separated by ;
# Note: Disabled for now due to URL encoding issues with semicolons
# TODO: Re-enable once we properly handle the bearings parameter
# if bearing is not None:
# bearing_parts = [f"{int(bearing) % 360},90"] + [''] * (len(all_coords) - 1)
# url += f"?bearings={';'.join(bearing_parts)}"
# return url, params
return url, params
def _parse_response(self, data: dict) -> Optional[Route]:
"""Parse OSRM API response into Route object."""
if data.get('code') != 'Ok' or not data.get('routes'):
return None
route_data = data['routes'][0]
steps = []
full_geometry = []
for leg in route_data.get('legs', []):
for step in leg.get('steps', []):
maneuver = step.get('maneuver', {})
# Parse geometry coordinates
geometry = []
for coord in step.get('geometry', {}).get('coordinates', []):
# OSRM uses [lon, lat] order
c = Coordinate(coord[1], coord[0])
geometry.append(c)
full_geometry.append(c)
step_name = step.get('name', '')
# Extract explicit maneuver location from OSRM
maneuver_location = maneuver.get('location') # [lon, lat]
maneuver_point = None
if maneuver_location and len(maneuver_location) == 2:
maneuver_point = Coordinate(maneuver_location[1], maneuver_location[0])
steps.append(Step(
distance=step.get('distance', 0),
duration=step.get('duration', 0),
name=step_name,
maneuver_type=maneuver.get('type', ''),
maneuver_modifier=maneuver.get('modifier', ''),
geometry=geometry,
speed_limit=None,
speed_limit_sign='vienna',
maneuver_point=maneuver_point,
))
return Route(
steps=steps,
distance=route_data.get('distance', 0),
duration=route_data.get('duration', 0),
geometry=full_geometry,
provider=self.name,
has_traffic=False,
raw=route_data,
)
@@ -0,0 +1,5 @@
"""Search providers."""
from .photon import PhotonSearchProvider
__all__ = ['PhotonSearchProvider']
@@ -0,0 +1,198 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Photon Search Provider.
Uses the free Photon (Komoot) geocoding API for address search.
No API key required, no rate limits.
Photon API: https://photon.komoot.io/
"""
from typing import Optional
import aiohttp
import requests
from ..base import SearchProvider
from ..models import Coordinate, SearchResult
PHOTON_URL = 'https://photon.komoot.io/api'
class PhotonSearchProvider(SearchProvider):
"""
Free Photon geocoding provider.
No API key required. Fast, reliable, uses OpenStreetMap data.
"""
name = "photon"
requires_api_key = False
def __init__(self):
"""Initialize Photon provider."""
pass
async def search(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Photon API (async)."""
if not query or len(query) < 2:
return []
params = {'q': query, 'limit': min(limit + 5, 15)} # Request extra for filtering
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
try:
async with aiohttp.ClientSession() as session:
async with session.get(PHOTON_URL, params=params, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status != 200:
return []
data = await resp.json()
except Exception:
return []
return self._parse_results(data, proximity, limit)
def search_sync(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Photon API (synchronous)."""
if not query or len(query) < 2:
return []
params = {'q': query, 'limit': min(limit + 5, 15)}
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
try:
resp = requests.get(PHOTON_URL, params=params, timeout=10)
if resp.status_code != 200:
return []
data = resp.json()
except Exception:
return []
return self._parse_results(data, proximity, limit)
async def reverse_geocode(
self,
coord: Coordinate
) -> Optional[SearchResult]:
"""Get address from coordinates using Photon reverse API."""
wgs_coord = coord.to_wgs84() if hasattr(coord, 'to_wgs84') else coord
url = f"{PHOTON_URL}/reverse"
params = {'lat': wgs_coord.latitude, 'lon': wgs_coord.longitude}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status != 200:
return None
data = await resp.json()
except Exception:
return None
features = data.get('features', [])
if not features:
return None
feature = features[0]
props = feature.get('properties', {})
parts = [props.get('name'), props.get('street'), props.get('city')]
address = ', '.join(filter(None, parts))
return SearchResult(
name=address.split(',')[0] if address else 'Unknown',
address=address or 'Unknown location',
coordinate=coord,
provider=self.name,
raw=feature,
)
def _parse_results(
self,
data: dict,
proximity: Optional[Coordinate],
limit: int
) -> list[SearchResult]:
"""Parse Photon GeoJSON response into SearchResult list."""
results = []
for feature in data.get('features', []):
try:
coords = feature['geometry']['coordinates']
props = feature.get('properties', {})
# Create coordinate (Photon uses [lon, lat] GeoJSON order)
coord = Coordinate(coords[1], coords[0])
# Build address from properties
parts = [
props.get('name'),
props.get('street'),
props.get('city'),
props.get('state'),
props.get('country')
]
address = ', '.join(filter(None, parts))
# Calculate distance if proximity provided
distance = None
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
search_coord = Coordinate(coords[1], coords[0]) # Use WGS84 for distance
distance = search_coord.distance_to(prox)
# Determine display name
name = props.get('name') or props.get('street') or (address.split(',')[0] if address else 'Unknown')
results.append(SearchResult(
name=name,
address=address or 'Unknown location',
coordinate=coord,
distance=distance,
place_id=props.get('osm_id'),
provider=self.name,
raw=feature,
))
except (KeyError, IndexError):
continue
# Sort by distance if proximity provided
if proximity:
results.sort(key=lambda r: r.distance if r.distance is not None else float('inf'))
return results[:limit]
@@ -0,0 +1,6 @@
"""Tile providers."""
# Import providers to register them with the factory
from .openfreemap import OpenFreeMapTileProvider
__all__ = ['OpenFreeMapTileProvider']
@@ -0,0 +1,88 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
OpenFreeMap Tile Provider.
Uses free OpenFreeMap tiles. No API key required.
OpenFreeMap: https://openfreemap.org/
"""
from ..base import TileProvider
from ..models import TileConfig
from ..factory import ProviderFactory
from ..config import TileProviderType
@ProviderFactory.register_tile(TileProviderType.OPENFREEMAP)
class OpenFreeMapTileProvider(TileProvider):
"""
Free OpenFreeMap tile provider.
No API key required. Uses OpenStreetMap data with nice styling.
"""
name = "openfreemap"
requires_api_key = False
def __init__(self, api_key: str = None):
"""Initialize OpenFreeMap provider."""
pass # No API key needed
def get_tile_config(self) -> TileConfig:
"""Get tile configuration."""
return TileConfig(
url_template="https://tiles.openfreemap.org/planet/{z}/{x}/{y}.pbf",
style_url="https://tiles.openfreemap.org/styles/liberty",
attribution='<a href="https://openfreemap.org">OpenFreeMap</a> | <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
min_zoom=0,
max_zoom=20,
)
def get_style_json(self) -> dict:
"""
Get MapLibre GL style JSON.
This style is optimized for navigation display with good contrast
and road visibility.
"""
return {
"version": 8,
"name": "OpenFreeMap Liberty",
"sources": {
"openmaptiles": {
"type": "vector",
"url": "https://tiles.openfreemap.org/planet"
}
},
"glyphs": "https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf",
"sprite": "https://tiles.openfreemap.org/styles/liberty/sprite",
"layers": [
# Simplified layer config - the actual style is loaded from style_url
# This is a fallback/reference
{
"id": "background",
"type": "background",
"paint": {"background-color": "#f8f4f0"}
}
]
}

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