Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 317d51566b | |||
| d606f7d723 | |||
| 1986516b32 | |||
| 6f274a1e59 | |||
| e889c3bf19 | |||
| 3346e4d281 | |||
| 80ac65a3e2 | |||
| e5ffea4734 | |||
| 3ef072024f | |||
| b5c149dfed | |||
| bdf701af10 | |||
| 0634122d32 | |||
| 5b95afd3dc | |||
| 01e9d51093 | |||
| 0076d30d62 | |||
| 6928314c89 |
@@ -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 }}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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"
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
@@ -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/**
|
|
||||||
|
|||||||
Vendored
+3
-1
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+15
-1
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+30
@@ -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.
|
||||||
@@ -1,75 +1,74 @@
|
|||||||
# StarPilot
|

|
||||||
|
|
||||||
[](https://deepwiki.com/firestar5683/StarPilot)
|
[Read this in English](README_EN.md)
|
||||||
[](https://firestar.link/discord)
|
|
||||||
[](https://github.com/firestar5683/StarPilot)
|
|
||||||
[](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` 是一種駕駛**輔助**系統,並非全自動駕駛。它旨在減輕您的駕駛疲勞,提升行車安全,但駕駛人仍需時刻保持專注,並隨時準備接管車輛。請務必遵守您所在地區的交通法規。
|
||||||
|
|
||||||
|
**最後,再次感謝您的到來。**
|
||||||
|
|
||||||
|
**期待與您一同在智慧駕駛的道路上,乘「龍」而行!**
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|

|
||||||
|
|
||||||
|
[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!**
|
||||||
@@ -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)`
|
||||||
|
|
||||||
|
[](https://github.com/commaai/openpilot/actions/workflows/tests.yaml)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://x.com/comma_ai)
|
||||||
|
[](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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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 "$@"
|
|
||||||
@@ -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 "$@"
|
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
@@ -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
@@ -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,2 +1 @@
|
|||||||
*.cpp
|
*.cpp
|
||||||
!params_pyx.cpp
|
|
||||||
|
|||||||
+3
-9
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+7
-68
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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.
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
bool watchdog_kick(uint64_t ts);
|
|
||||||
@@ -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
|
|
||||||
@@ -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" "$@"
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
Executable
+598
@@ -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()
|
||||||
Executable
+423
@@ -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()
|
||||||
@@ -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
|
||||||
|
"""
|
||||||
@@ -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))
|
||||||
@@ -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',
|
||||||
|
]
|
||||||
@@ -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) = v² × 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/s²)
|
||||||
|
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 = v² × κ
|
||||||
|
|
||||||
|
Args:
|
||||||
|
v_ego: Vehicle speed in m/s
|
||||||
|
curvature: Road curvature in 1/m
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lateral acceleration in m/s²
|
||||||
|
"""
|
||||||
|
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/s²
|
||||||
|
"""
|
||||||
|
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/s² 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² - 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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+919
@@ -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/s² 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()
|
||||||
Executable
+566
@@ -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',
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user