mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-09 09:34:35 +08:00
Compare commits
66 Commits
feat/vibe-
...
clippy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7760793ab1 | ||
|
|
dd074cb6ef | ||
|
|
c1d3ae427b | ||
|
|
2ab45b552d | ||
|
|
8c1d59fecd | ||
|
|
cde88fd8ed | ||
|
|
4b5de0eddb | ||
|
|
071147baaf | ||
|
|
18af4d6ad6 | ||
|
|
b81d5bca3c | ||
|
|
682d738ffa | ||
|
|
f60c2b6a83 | ||
|
|
f833819143 | ||
|
|
707e2aedae | ||
|
|
55147d8a55 | ||
|
|
de7acc5466 | ||
|
|
e4aada10a4 | ||
|
|
4a613aa0e4 | ||
|
|
b460d5804c | ||
|
|
eecb8e5c19 | ||
|
|
1a4ea66987 | ||
|
|
c1e15e5544 | ||
|
|
3a45fff1b9 | ||
|
|
ae9bd39883 | ||
|
|
43e7d87176 | ||
|
|
432c6050ed | ||
|
|
4e3b1f1f6b | ||
|
|
5d47ffdb8a | ||
|
|
1c89e2b885 | ||
|
|
c552567ada | ||
|
|
7097e69aa3 | ||
|
|
657ff0f8ec | ||
|
|
641af6d7e7 | ||
|
|
f57de1c5b2 | ||
|
|
cb5d120136 | ||
|
|
c85b6a0d1c | ||
|
|
025a930ce8 | ||
|
|
523c92c6fe | ||
|
|
72282f2d2e | ||
|
|
2825c00fcc | ||
|
|
063aa994d2 | ||
|
|
50462a1d01 | ||
|
|
a95d91f77a | ||
|
|
437726b348 | ||
|
|
9e6af5ba74 | ||
|
|
99bd9075d5 | ||
|
|
c438aeb5a5 | ||
|
|
f1ca81debf | ||
|
|
d7e1c42c2b | ||
|
|
6d51d64285 | ||
|
|
e0ccc175e4 | ||
|
|
734151f59b | ||
|
|
9a14baac4d | ||
|
|
d3e3628a95 | ||
|
|
fec6382b96 | ||
|
|
4bd020e92b | ||
|
|
7f5342f378 | ||
|
|
339bc0b8b3 | ||
|
|
59c64acc29 | ||
|
|
7229c7541e | ||
|
|
39e73cc46e | ||
|
|
285fd97606 | ||
|
|
e5f1f86ac2 | ||
|
|
8b210c9bdb | ||
|
|
ebc2cf1da7 | ||
|
|
0de4dfcafc |
@@ -3,3 +3,4 @@ REGIST
|
||||
PullRequest
|
||||
cancelled
|
||||
FOF
|
||||
NoO
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
env:
|
||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||
run: |
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||
cd gitlab_docs
|
||||
git checkout main
|
||||
git sparse-checkout set --no-cone models/
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||
run: |
|
||||
echo "Cloning GitLab"
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||
cd gitlab_docs
|
||||
echo "checkout models/${RECOMPILED_DIR}"
|
||||
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
||||
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts'
|
||||
run: |
|
||||
echo "Cloning GitLab"
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs
|
||||
git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs
|
||||
cd gitlab_docs
|
||||
echo "checkout models/${RECOMPILED_DIR}"
|
||||
git sparse-checkout set --no-cone models/${RECOMPILED_DIR}
|
||||
|
||||
105
.github/workflows/post-to-discourse/action.yml
vendored
Normal file
105
.github/workflows/post-to-discourse/action.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: 'Post to Discourse'
|
||||
description: 'Posts a message to a Discourse topic (existing or new)'
|
||||
|
||||
inputs:
|
||||
discourse-url:
|
||||
description: 'Discourse instance URL (e.g., https://discourse.example.com)'
|
||||
required: true
|
||||
api-key:
|
||||
description: 'Discourse API key'
|
||||
required: true
|
||||
api-username:
|
||||
description: 'Discourse API username'
|
||||
required: true
|
||||
topic-id:
|
||||
description: 'Discourse topic ID to post to (use this OR category-id + title)'
|
||||
required: false
|
||||
category-id:
|
||||
description: 'Category ID for new topic (required if topic-id not provided)'
|
||||
required: false
|
||||
title:
|
||||
description: 'Title for new topic (required if topic-id not provided)'
|
||||
required: false
|
||||
message:
|
||||
description: 'Message content (markdown supported)'
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
post-number:
|
||||
description: 'The post number in the topic'
|
||||
value: ${{ steps.post.outputs.post_number }}
|
||||
post-url:
|
||||
description: 'Direct URL to the post'
|
||||
value: ${{ steps.post.outputs.post_url }}
|
||||
topic-id:
|
||||
description: 'The topic ID (useful when creating a new topic)'
|
||||
value: ${{ steps.post.outputs.topic_id }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Post to Discourse
|
||||
id: post
|
||||
shell: bash
|
||||
run: |
|
||||
# Validate inputs
|
||||
if [ -z "${{ inputs.topic-id }}" ] && ([ -z "${{ inputs.category-id }}" ] || [ -z "${{ inputs.title }}" ]); then
|
||||
echo "❌ Error: Must provide either topic-id OR both category-id and title"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${{ inputs.topic-id }}" ] && ([ -n "${{ inputs.category-id }}" ] || [ -n "${{ inputs.title }}" ]); then
|
||||
echo "⚠️ Warning: Both topic-id and category-id/title provided. Will post to existing topic."
|
||||
fi
|
||||
|
||||
# Determine if creating new topic or posting to existing
|
||||
if [ -n "${{ inputs.topic-id }}" ]; then
|
||||
echo "📝 Posting to existing topic ID: ${{ inputs.topic-id }}"
|
||||
|
||||
# Create JSON payload for posting to existing topic
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg content '${{ inputs.message }}' \
|
||||
--arg topic_id "${{ inputs.topic-id }}" \
|
||||
'{topic_id: $topic_id, raw: $content}')
|
||||
else
|
||||
echo "✨ Creating new topic: ${{ inputs.title }}"
|
||||
|
||||
# Create JSON payload for new topic
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg content '${{ inputs.message }}' \
|
||||
--arg title "${{ inputs.title }}" \
|
||||
--arg category "${{ inputs.category-id }}" \
|
||||
'{title: $title, category: ($category | tonumber), raw: $content}')
|
||||
fi
|
||||
|
||||
# Post to Discourse
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
||||
-X POST "${{ inputs.discourse-url }}/posts.json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Api-Key: ${{ inputs.api-key }}" \
|
||||
-H "Api-Username: ${{ inputs.api-username }}" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✅ Successfully posted to Discourse!"
|
||||
|
||||
POST_NUMBER=$(echo "$BODY" | jq -r '.post_number // "unknown"')
|
||||
TOPIC_ID=$(echo "$BODY" | jq -r '.topic_id // "${{ inputs.topic-id }}"')
|
||||
POST_URL="${{ inputs.discourse-url }}/t/${TOPIC_ID}/${POST_NUMBER}"
|
||||
|
||||
echo "post_number=${POST_NUMBER}" >> $GITHUB_OUTPUT
|
||||
echo "post_url=${POST_URL}" >> $GITHUB_OUTPUT
|
||||
echo "topic_id=${TOPIC_ID}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Topic ID: ${TOPIC_ID}"
|
||||
echo "Post number: ${POST_NUMBER}"
|
||||
echo "URL: ${POST_URL}"
|
||||
else
|
||||
echo "❌ Failed to post to Discourse"
|
||||
echo "HTTP Code: ${HTTP_CODE}"
|
||||
echo "Response: ${BODY}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -156,6 +156,8 @@ jobs:
|
||||
with:
|
||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||
path: ${{ github.workspace }}/selfdrive/modeld/models
|
||||
- run: |
|
||||
rm -f ${{ github.workspace }}/selfdrive/modeld/models/{dmonitoring_model,big_driving_policy,big_driving_vision}.onnx
|
||||
|
||||
- name: Build Model
|
||||
run: |
|
||||
|
||||
65
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
65
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')";
|
||||
echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT
|
||||
|
||||
stable_version=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g');
|
||||
stable_version=$(cat sunnypilot/common/version.h | grep SUNNYPILOT_VERSION | sed -e 's/[^0-9|.]//g');
|
||||
echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT
|
||||
echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -302,36 +302,51 @@ jobs:
|
||||
git push -f origin ${TAG}
|
||||
|
||||
notify:
|
||||
needs: [ build, publish ]
|
||||
needs:
|
||||
- prepare_strategy
|
||||
- build
|
||||
- publish
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ (always() && !cancelled() && !failure()) && needs.publish.result == 'success' && !failure() && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
|
||||
if: ${{ (always() && !cancelled() && !failure())
|
||||
&& needs.publish.result == 'success'
|
||||
&& (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
&& (fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES_V2)[github.head_ref || github.ref_name] != null) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Alpine Linux environment
|
||||
uses: jirutka/setup-alpine@v1.2.0
|
||||
with:
|
||||
packages: 'jq gettext curl'
|
||||
|
||||
- name: Send Discord Notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ contains(fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES), env.SOURCE_BRANCH) && secrets.DISCORD_DEV_FEEDBACK_CHANNEL_WEBHOOK || secrets.DISCORD_DEV_PRIVATE_CHANNEL_WEBHOOK }}
|
||||
- name: Prepare notification message
|
||||
id: message
|
||||
run: |
|
||||
TEMPLATE='${{ vars.DISCORD_GENERAL_UPDATE_NOTICE }}'
|
||||
export EXTRA_VERSION_IDENTIFIER="${{ needs.build.outputs.extra_version_identifier }}"
|
||||
export VERSION="${{ needs.build.outputs.version }}"
|
||||
export branch_name=${{ env.SOURCE_BRANCH }}
|
||||
export new_branch=${{ needs.build.outputs.new_branch }}
|
||||
export extra_version_identifier=${{ needs.build.outputs.extra_version_identifier || github.run_number}}
|
||||
echo ${TEMPLATE} | envsubst | jq -c '.' | tee payload.json
|
||||
curl -X POST -H "Content-Type: application/json" -d @payload.json $DISCORD_WEBHOOK
|
||||
TEMPLATE='${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}'
|
||||
export VERSION="${{ needs.prepare_strategy.outputs.version }}"
|
||||
export branch_name="${{ env.SOURCE_BRANCH }}"
|
||||
export new_branch="${{ needs.prepare_strategy.outputs.new_branch }}"
|
||||
export commit_sha="${{ github.sha }}"
|
||||
export commit_short_sha="${{ github.sha }}"
|
||||
export commit_short_sha="${commit_short_sha:0:7}"
|
||||
export extra_version_identifier="${{ needs.prepare_strategy.outputs.extra_version_identifier || github.run_number }}"
|
||||
export PUBLIC_REPO_URL="${{ env.PUBLIC_REPO_URL }}"
|
||||
|
||||
echo ""
|
||||
echo "---- ℹ️ To update the list of branches that notify to dev-feedback -----"
|
||||
echo ""
|
||||
echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/DEV_FEEDBACK_NOTIFICATION_BRANCHES"
|
||||
echo "2. Current value: ${{ vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES }}"
|
||||
echo "3. Update as needed (JSON array with no spaces)"
|
||||
shell: alpine.sh {0}
|
||||
MESSAGE=$(cat << 'EOF' | envsubst
|
||||
${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}
|
||||
EOF
|
||||
)
|
||||
|
||||
{
|
||||
echo 'content<<EOFMARKER'
|
||||
echo "$MESSAGE"
|
||||
echo 'EOFMARKER'
|
||||
} >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Post to Discourse
|
||||
uses: ./.github/workflows/post-to-discourse
|
||||
with:
|
||||
discourse-url: ${{ vars.DISCOURSE_URL }}
|
||||
api-key: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
api-username: "system"
|
||||
topic-id: ${{ fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES_V2)[github.head_ref || github.ref_name].topic_id }}
|
||||
message: ${{ steps.message.outputs.content }}
|
||||
|
||||
manage-pr-labels:
|
||||
name: Remove prebuilt label
|
||||
|
||||
@@ -3,7 +3,6 @@ name: Build dev
|
||||
env:
|
||||
DEFAULT_SOURCE_BRANCH: "master"
|
||||
DEFAULT_TARGET_BRANCH: "master-dev"
|
||||
PR_LABEL: "dev"
|
||||
LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs'
|
||||
LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git'
|
||||
|
||||
@@ -43,7 +42,7 @@ jobs:
|
||||
if: (
|
||||
(github.event_name == 'workflow_dispatch')
|
||||
|| (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch))
|
||||
|| (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev'))))
|
||||
|| (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == vars.PREBUILT_PR_LABEL || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, vars.PREBUILT_PR_LABEL))))
|
||||
)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -55,7 +54,7 @@ jobs:
|
||||
uses: ./.github/workflows/wait-for-action # Path to where you place the action
|
||||
if: (
|
||||
(github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch))
|
||||
|| (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev'))))
|
||||
|| (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == vars.PREBUILT_PR_LABEL || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, vars.PREBUILT_PR_LABEL))))
|
||||
)
|
||||
with:
|
||||
workflow: selfdrive_tests.yaml # The workflow file to monitor
|
||||
@@ -119,7 +118,7 @@ jobs:
|
||||
# Use GitHub API to get PRs with specific label, ordered by creation date
|
||||
PR_LIST=$(gh api graphql -f query='
|
||||
query($search_query:String!) {
|
||||
search(query: $search_query, type:ISSUE, first:100) {
|
||||
search(query: $search_query, type:ISSUE, first:40) {
|
||||
nodes {
|
||||
... on PullRequest {
|
||||
number
|
||||
@@ -149,7 +148,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -F search_query="repo:${{ github.repository }} is:pr is:open label:${PR_LABEL},${PR_LABEL}-c3 draft:false sort:created-asc")
|
||||
}' -F search_query="repo:${{ github.repository }} is:pr is:open label:${{ vars.PREBUILT_PR_LABEL }},${{ vars.PREBUILT_PR_LABEL }}-c3 draft:false sort:created-asc")
|
||||
|
||||
PR_LIST=${PR_LIST//\'/}
|
||||
echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT
|
||||
|
||||
78
.github/workflows/test-discourse.yaml.yml
vendored
Normal file
78
.github/workflows/test-discourse.yaml.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Debug Discourse Posting
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
test-discourse-post:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Post test message to Discourse
|
||||
uses: ./.github/workflows/post-to-discourse
|
||||
with:
|
||||
discourse-url: ${{ vars.DISCOURSE_URL }}
|
||||
api-key: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
|
||||
topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
|
||||
message: |
|
||||
## 🧪 Test Post from GitHub Actions
|
||||
|
||||
**This is a test post to verify Discourse integration**
|
||||
|
||||
- **Workflow**: ${{ github.workflow }}
|
||||
- **Run Number**: #${{ github.run_number }}
|
||||
- **Branch**: `${{ github.ref_name }}`
|
||||
- **Commit**: ${{ github.sha }}
|
||||
- **Actor**: @${{ github.actor }}
|
||||
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
---
|
||||
|
||||
### Fake Build Info (for testing)
|
||||
- **Version**: 0.9.8-test
|
||||
- **Build**: #42
|
||||
- **Branch**: release-test
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
*This is an automated test message. Drive safe! 🚗💨*
|
||||
|
||||
|
||||
- name: Create topic on Discourse
|
||||
uses: ./.github/workflows/post-to-discourse
|
||||
with:
|
||||
discourse-url: ${{ vars.DISCOURSE_URL }}
|
||||
api-key: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
|
||||
#topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
|
||||
category-id: 4
|
||||
title: "This is a test of a new topic instead of a reply"
|
||||
message: |
|
||||
## 🧪 Test Post from GitHub Actions
|
||||
|
||||
**This is a test post to verify Discourse integration**
|
||||
|
||||
- **Workflow**: ${{ github.workflow }}
|
||||
- **Run Number**: #${{ github.run_number }}
|
||||
- **Branch**: `${{ github.ref_name }}`
|
||||
- **Commit**: ${{ github.sha }}
|
||||
- **Actor**: @${{ github.actor }}
|
||||
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
---
|
||||
|
||||
### Fake Build Info (for testing)
|
||||
- **Version**: 0.9.8-test
|
||||
- **Build**: #42
|
||||
- **Branch**: release-test
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
*This is an automated test message. Drive safe! 🚗💨*
|
||||
- name: Display results
|
||||
if: always()
|
||||
run: |
|
||||
echo "::notice::Discourse post test completed"
|
||||
echo "Check your Discourse topic to verify the post appeared correctly"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -109,3 +109,7 @@ Pipfile
|
||||
!.idea/customTargets.xml
|
||||
!.idea/tools/*
|
||||
!.run/*
|
||||
|
||||
### clippy ###
|
||||
clippy_stats.json
|
||||
clippy.log
|
||||
|
||||
1104
CHANGELOG.md
Normal file
1104
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
36
README.md
36
README.md
@@ -3,11 +3,9 @@
|
||||
## 🌞 What is sunnypilot?
|
||||
[sunnypilot](https://github.com/sunnyhaibin/sunnypilot) is a fork of comma.ai's openpilot, an open source driver assistance system. sunnypilot offers the user a unique driving experience for over 300+ supported car makes and models with modified behaviors of driving assist engagements. sunnypilot complies with comma.ai's safety rules as accurately as possible.
|
||||
|
||||
## 💭 Join our Discord
|
||||
Join the official sunnypilot Discord server to stay up to date with all the latest features and be a part of shaping the future of sunnypilot!
|
||||
* https://discord.gg/sunnypilot
|
||||
|
||||
 
|
||||
## 💭 Join our Community Forum
|
||||
Join the official sunnypilot community forum to stay up to date with all the latest features and be a part of shaping the future of sunnypilot!
|
||||
* https://community.sunnypilot.ai/
|
||||
|
||||
## Documentation
|
||||
https://docs.sunnypilot.ai/ is your one stop shop for everything from features to installation to FAQ about the sunnypilot
|
||||
@@ -16,13 +14,13 @@ https://docs.sunnypilot.ai/ is your one stop shop for everything from features t
|
||||
* A supported device to run this software
|
||||
* a [comma three](https://comma.ai/shop/products/three) or a [C3X](https://comma.ai/shop/comma-3x)
|
||||
* This software
|
||||
* One of [the 300+ supported cars](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot.
|
||||
* One of [the 325+ supported cars](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford, and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot.
|
||||
* A [car harness](https://comma.ai/shop/products/car-harness) to connect to your car
|
||||
|
||||
Detailed instructions for [how to mount the device in a car](https://comma.ai/setup).
|
||||
|
||||
## Installation
|
||||
Please refer to [Recommended Branches](#-recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging-c3-new` branch.
|
||||
Please refer to [Recommended Branches](#recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging` branch.
|
||||
|
||||
### If you want to use our newest branches (our rewrite)
|
||||
> [!TIP]
|
||||
@@ -31,28 +29,28 @@ Please refer to [Recommended Branches](#-recommended-branches) to find your pref
|
||||
* sunnypilot not installed or you installed a version before 0.8.17?
|
||||
1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed.
|
||||
2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option.
|
||||
3. Input the installation URL per [Recommended Branches](#-recommended-branches). Example: ```https://staging-c3-new.sunnypilot.ai```.
|
||||
3. Input the installation URL per [Recommended Branches](#recommended-branches). Example: ```https://staging.sunnypilot.ai```.
|
||||
4. Complete the rest of the installation following the onscreen instructions.
|
||||
|
||||
* sunnypilot already installed and you installed a version after 0.8.17?
|
||||
1. On the comma three, go to `Settings` ▶️ `Software`.
|
||||
1. On the comma three/3X, go to `Settings` ▶️ `Software`.
|
||||
2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot.
|
||||
3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector.
|
||||
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging-c3-new`
|
||||
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging`
|
||||
|
||||
|
||||
| Branch | Installation URL |
|
||||
|:----------------:|:---------------------------------------------:|
|
||||
| `staging-c3-new` | `https://staging-c3-new.sunnypilot.ai` |
|
||||
| `dev-c3-new` | `https://dev-c3-new.sunnypilot.ai` |
|
||||
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
|
||||
| `release-c3-new` | **Not yet available**. |
|
||||
### Recommended Branches
|
||||
| Branch | Installation URL |
|
||||
|:---------------:|:---------------------------------------------:|
|
||||
| `release` | `https://release.sunnypilot.ai` |
|
||||
| `staging` | `https://staging.sunnypilot.ai` |
|
||||
| `dev` | `https://dev.sunnypilot.ai` |
|
||||
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
|
||||
|
||||
> [!TIP]
|
||||
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging-c3-new'.
|
||||
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging'.
|
||||
|
||||
> [!NOTE]
|
||||
> Do you require further assistance with software installation? Join the [sunnypilot Discord server](https://discord.sunnypilot.com) and message us in the `#installation-help` channel.
|
||||
> Do you require further assistance with software installation? Join the [sunnypilot community forum](https://community.sunnypilot.ai/new-topic?category=general/qa) and create a topic in the General/Q&A Category channel.
|
||||
|
||||
|
||||
<details>
|
||||
|
||||
@@ -69,6 +69,48 @@ struct LeadData {
|
||||
struct SelfdriveStateSP @0x81c2f05a394cf4af {
|
||||
mads @0 :ModularAssistiveDrivingSystem;
|
||||
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
|
||||
|
||||
enum AudibleAlert {
|
||||
none @0;
|
||||
|
||||
engage @1;
|
||||
disengage @2;
|
||||
refuse @3;
|
||||
|
||||
warningSoft @4;
|
||||
warningImmediate @5;
|
||||
|
||||
prompt @6;
|
||||
promptRepeat @7;
|
||||
promptDistracted @8;
|
||||
|
||||
# unused, these are reserved for upstream events so we don't collide
|
||||
reserved9 @9;
|
||||
reserved10 @10;
|
||||
reserved11 @11;
|
||||
reserved12 @12;
|
||||
reserved13 @13;
|
||||
reserved14 @14;
|
||||
reserved15 @15;
|
||||
reserved16 @16;
|
||||
reserved17 @17;
|
||||
reserved18 @18;
|
||||
reserved19 @19;
|
||||
reserved20 @20;
|
||||
reserved21 @21;
|
||||
reserved22 @22;
|
||||
reserved23 @23;
|
||||
reserved24 @24;
|
||||
reserved25 @25;
|
||||
reserved26 @26;
|
||||
reserved27 @27;
|
||||
reserved28 @28;
|
||||
reserved29 @29;
|
||||
reserved30 @30;
|
||||
|
||||
promptSingleLow @31;
|
||||
promptSingleHigh @32;
|
||||
}
|
||||
}
|
||||
|
||||
struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
@@ -404,12 +446,12 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
|
||||
|
||||
struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
laneTurnDirection @0 :TurnDirection;
|
||||
}
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
turnLeft @1;
|
||||
turnRight @2;
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
turnLeft @1;
|
||||
turnRight @2;
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomReserved10 @0xcb9fd56c7057593a {
|
||||
|
||||
@@ -154,15 +154,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
|
||||
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
||||
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
||||
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "100"}},
|
||||
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"OnroadScreenOffControl", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
|
||||
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
@@ -205,6 +206,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
|
||||
// sunnypilot car specific params
|
||||
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "common/version.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
#include "sunnypilot/common/version.h"
|
||||
|
||||
class SwaglogState {
|
||||
public:
|
||||
SwaglogState() {
|
||||
@@ -56,7 +58,7 @@ public:
|
||||
if (char* daemon_name = getenv("MANAGER_DAEMON")) {
|
||||
ctx_j["daemon"] = daemon_name;
|
||||
}
|
||||
ctx_j["version"] = COMMA_VERSION;
|
||||
ctx_j["version"] = SUNNYPILOT_VERSION;
|
||||
ctx_j["dirty"] = !getenv("CLEAN");
|
||||
ctx_j["device"] = Hardware::get_name();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ from openpilot.common.markdown import parse_markdown
|
||||
|
||||
class TestMarkdown:
|
||||
def test_all_release_notes(self):
|
||||
with open(os.path.join(BASEDIR, "RELEASES.md")) as f:
|
||||
with open(os.path.join(BASEDIR, "CHANGELOG.md")) as f:
|
||||
release_notes = f.read().split("\n\n")
|
||||
assert len(release_notes) > 10
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "system/hardware/hw.h"
|
||||
#include "third_party/json11/json11.hpp"
|
||||
|
||||
#include "sunnypilot/common/version.h"
|
||||
|
||||
std::string daemon_name = "testy";
|
||||
std::string dongle_id = "test_dongle_id";
|
||||
int LINE_NO = 0;
|
||||
@@ -53,7 +55,7 @@ void recv_log(int thread_cnt, int thread_msg_cnt) {
|
||||
REQUIRE(ctx["dongle_id"].string_value() == dongle_id);
|
||||
REQUIRE(ctx["dirty"].bool_value() == true);
|
||||
|
||||
REQUIRE(ctx["version"].string_value() == COMMA_VERSION);
|
||||
REQUIRE(ctx["version"].string_value() == SUNNYPILOT_VERSION);
|
||||
|
||||
std::string device = Hardware::get_name();
|
||||
REQUIRE(ctx["device"].string_value() == device);
|
||||
|
||||
30
docs/CARS.md
30
docs/CARS.md
@@ -4,7 +4,7 @@
|
||||
|
||||
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
|
||||
|
||||
# 337 Supported Cars
|
||||
# 339 Supported Cars
|
||||
|
||||
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|Setup Video|
|
||||
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
@@ -21,7 +21,10 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2017">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EV Non-ACC 2018-21|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2018-21">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Equinox 2019-22">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Malibu Non-ACC 2016-23|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Malibu Non-ACC 2016-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Silverado 1500 2020-21">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Trailblazer 2021-22">Buy Here</a></sub></details>|||
|
||||
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica 2017-18">Buy Here</a></sub></details>|||
|
||||
@@ -236,20 +239,20 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Rivian A connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|
||||
|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2020-22|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2020-22|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Škoda|Fabia 2022-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Škoda|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
|
||||
@@ -308,7 +311,6 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Sienna 2018-20|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Wildlander PHEV 2021|All|openpilot|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Wildlander PHEV 2021">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|
||||
Submodule opendbc_repo updated: b592ecdd3b...c32e79f3c6
@@ -73,6 +73,10 @@ dependencies = [
|
||||
|
||||
# ui
|
||||
"qrcode",
|
||||
|
||||
# clippy
|
||||
"discord-py",
|
||||
"flask",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -39,7 +39,7 @@ cd $BUILD_DIR
|
||||
rm -f panda/board/obj/panda.bin.signed
|
||||
rm -f panda/board/obj/panda_h7.bin.signed
|
||||
|
||||
VERSION=$(cat common/version.h | awk -F[\"-] '{print $2}')
|
||||
VERSION=$(cat sunnypilot/common/version.h | awk -F[\"-] '{print $2}')
|
||||
echo "[-] committing version $VERSION T=$SECONDS"
|
||||
git add -f .
|
||||
git commit -a -m "openpilot v$VERSION release"
|
||||
|
||||
@@ -49,7 +49,7 @@ rm -f panda/board/obj/panda.bin.signed
|
||||
GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD)
|
||||
GIT_COMMIT_DATE=$(git --git-dir=$SOURCE_DIR/.git show --no-patch --format='%ct %ci' HEAD)
|
||||
DATETIME=$(date '+%Y-%m-%dT%H:%M:%S')
|
||||
VERSION=$(cat $SOURCE_DIR/common/version.h | awk -F\" '{print $2}')
|
||||
VERSION=$(cat $SOURCE_DIR/sunnypilot/common/version.h | awk -F\" '{print $2}')
|
||||
|
||||
echo -n "$GIT_HASH" > git_src_commit
|
||||
echo -n "$GIT_COMMIT_DATE" > git_src_commit_date
|
||||
|
||||
@@ -30,7 +30,7 @@ if [ -z "$GIT_ORIGIN" ]; then
|
||||
fi
|
||||
|
||||
# "Tagging"
|
||||
echo "#define COMMA_VERSION \"$VERSION\"" > ${OUTPUT_DIR}/common/version.h
|
||||
echo "#define SUNNYPILOT_VERSION \"$VERSION\"" > ${OUTPUT_DIR}/sunnypilot/common/version.h
|
||||
|
||||
## set git identity
|
||||
#source $DIR/identity.sh
|
||||
@@ -55,7 +55,7 @@ git add -f .
|
||||
# include source commit hash and build date in commit
|
||||
GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD)
|
||||
DATETIME=$(date '+%Y-%m-%dT%H:%M:%S')
|
||||
SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/common/version.h)
|
||||
SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/sunnypilot/common/version.h)
|
||||
|
||||
# Commit with detailed message
|
||||
git commit -a -m "sunnypilot v$VERSION
|
||||
|
||||
BIN
selfdrive/assets/sounds/prompt_single_high.wav
LFS
Normal file
BIN
selfdrive/assets/sounds/prompt_single_high.wav
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/sounds/prompt_single_low.wav
LFS
Normal file
BIN
selfdrive/assets/sounds/prompt_single_low.wav
LFS
Normal file
Binary file not shown.
@@ -88,6 +88,7 @@ class Car:
|
||||
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
|
||||
|
||||
is_release = self.params.get_bool("IsReleaseBranch")
|
||||
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
|
||||
|
||||
if CI is None:
|
||||
# wait for one pandaState and one CAN packet
|
||||
@@ -110,7 +111,7 @@ class Car:
|
||||
init_params_list_sp = sunnypilot_interfaces.initialize_params(self.params)
|
||||
|
||||
self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params,
|
||||
fixed_fingerprint, init_params_list_sp)
|
||||
fixed_fingerprint, init_params_list_sp, is_release_sp)
|
||||
sunnypilot_interfaces.setup_interfaces(self.CI, self.params)
|
||||
self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP, self.CI.CP_SP)
|
||||
self.CP = self.CI.CP
|
||||
|
||||
@@ -151,7 +151,7 @@ class TestCarModelBase(unittest.TestCase):
|
||||
|
||||
cls.CarInterface = interfaces[cls.platform]
|
||||
cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, alpha_long, False, docs=False)
|
||||
cls.CP_SP = cls.CarInterface.get_params_sp(cls.CP, cls.platform, cls.fingerprint, car_fw, alpha_long, docs=False)
|
||||
cls.CP_SP = cls.CarInterface.get_params_sp(cls.CP, cls.platform, cls.fingerprint, car_fw, alpha_long, False, docs=False)
|
||||
assert cls.CP
|
||||
assert cls.CP_SP
|
||||
assert cls.CP.carFingerprint == cls.platform
|
||||
|
||||
@@ -99,7 +99,6 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
|
||||
self.LaC.extension.update_model_v2(self.sm['modelV2'])
|
||||
|
||||
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
|
||||
self.LaC.extension.update_lateral_lag(self.lat_delay)
|
||||
|
||||
long_plan = self.sm['longitudinalPlan']
|
||||
@@ -133,7 +132,7 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
self.LoC.reset()
|
||||
|
||||
# accel PID loop
|
||||
pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, CS.vCruise * CV.KPH_TO_MS)
|
||||
pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, self.CP_SP, CS.vEgo, CS.vCruise * CV.KPH_TO_MS)
|
||||
actuators.accel = float(self.LoC.update(CC.longActive, CS, long_plan.aTarget, long_plan.shouldStop, pid_accel_limits))
|
||||
|
||||
# Steering PID loop and lateral MPC
|
||||
@@ -234,6 +233,9 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
while not evt.is_set():
|
||||
self.get_params_sp()
|
||||
|
||||
if self.CP.lateralTuning.which() == 'torque':
|
||||
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTur
|
||||
|
||||
LaneChangeState = log.LaneChangeState
|
||||
LaneChangeDirection = log.LaneChangeDirection
|
||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||
|
||||
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
|
||||
LANE_CHANGE_TIME_MAX = 10.
|
||||
@@ -32,9 +33,9 @@ DESIRES = {
|
||||
}
|
||||
|
||||
TURN_DESIRES = {
|
||||
custom.TurnDirection.none: log.Desire.none,
|
||||
custom.TurnDirection.turnLeft: log.Desire.turnLeft,
|
||||
custom.TurnDirection.turnRight: log.Desire.turnRight,
|
||||
TurnDirection.none: log.Desire.none,
|
||||
TurnDirection.turnLeft: log.Desire.turnLeft,
|
||||
TurnDirection.turnRight: log.Desire.turnRight,
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +50,7 @@ class DesireHelper:
|
||||
self.desire = log.Desire.none
|
||||
self.alc = AutoLaneChangeController(self)
|
||||
self.lane_turn_controller = LaneTurnController(self)
|
||||
self.lane_turn_direction = custom.TurnDirection.none
|
||||
self.lane_turn_direction = TurnDirection.none
|
||||
|
||||
@staticmethod
|
||||
def get_lane_change_direction(CS):
|
||||
@@ -126,7 +127,7 @@ class DesireHelper:
|
||||
|
||||
self.prev_one_blinker = one_blinker
|
||||
|
||||
if self.lane_turn_direction != custom.TurnDirection.none:
|
||||
if self.lane_turn_direction != TurnDirection.none:
|
||||
self.desire = TURN_DESIRES[self.lane_turn_direction]
|
||||
else:
|
||||
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
|
||||
|
||||
@@ -51,12 +51,12 @@ def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
|
||||
|
||||
|
||||
class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
|
||||
def __init__(self, CP, CP_SP, init_v=0.0, init_a=0.0, dt=DT_MDL):
|
||||
self.CP = CP
|
||||
self.mpc = LongitudinalMpc(dt=dt)
|
||||
# TODO remove mpc modes when TR released
|
||||
self.mpc.mode = 'acc'
|
||||
LongitudinalPlannerSP.__init__(self, self.CP, self.mpc)
|
||||
LongitudinalPlannerSP.__init__(self, self.CP, CP_SP, self.mpc)
|
||||
self.fcw = False
|
||||
self.dt = dt
|
||||
self.allow_throttle = True
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from cereal import car
|
||||
from cereal import car, custom
|
||||
from openpilot.common.gps import get_gps_location_service
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import Priority, config_realtime_process
|
||||
@@ -17,10 +17,14 @@ def main():
|
||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||
cloudlog.info("plannerd got CarParams: %s", CP.brand)
|
||||
|
||||
cloudlog.info("plannerd is waiting for CarParamsSP")
|
||||
CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP)
|
||||
cloudlog.info("plannerd got CarParamsSP")
|
||||
|
||||
gps_location_service = get_gps_location_service(params)
|
||||
|
||||
ldw = LaneDepartureWarning()
|
||||
longitudinal_planner = LongitudinalPlanner(CP)
|
||||
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
|
||||
'liveMapDataSP', 'carStateSP', gps_location_service],
|
||||
|
||||
@@ -24,6 +24,7 @@ from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroa
|
||||
from openpilot.system.version import get_build_metadata
|
||||
|
||||
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
|
||||
from openpilot.sunnypilot import get_sanitize_int_param
|
||||
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
||||
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
||||
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
||||
@@ -43,6 +44,7 @@ LaneChangeDirection = log.LaneChangeDirection
|
||||
EventName = log.OnroadEvent.EventName
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
SafetyModel = car.CarParams.SafetyModel
|
||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||
|
||||
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
|
||||
|
||||
@@ -130,7 +132,12 @@ class SelfdriveD(CruiseHelper):
|
||||
self.logged_comm_issue = None
|
||||
self.not_running_prev = None
|
||||
self.experimental_mode = False
|
||||
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
||||
self.personality = get_sanitize_int_param(
|
||||
"LongitudinalPersonality",
|
||||
min(log.LongitudinalPersonality.schema.enumerants.values()),
|
||||
max(log.LongitudinalPersonality.schema.enumerants.values()),
|
||||
self.params
|
||||
)
|
||||
self.recalibrating_seen = False
|
||||
self.state_machine = StateMachine()
|
||||
self.rk = Ratekeeper(100, print_delay_threshold=None)
|
||||
@@ -299,9 +306,9 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
# Handle lane turn
|
||||
lane_turn_direction = self.sm['modelDataV2SP'].laneTurnDirection
|
||||
if lane_turn_direction == custom.TurnDirection.turnLeft:
|
||||
if lane_turn_direction == TurnDirection.turnLeft:
|
||||
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnLeft)
|
||||
elif lane_turn_direction == custom.TurnDirection.turnRight:
|
||||
elif lane_turn_direction == TurnDirection.turnRight:
|
||||
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnRight)
|
||||
|
||||
for i, pandaState in enumerate(self.sm['pandaStates']):
|
||||
|
||||
@@ -51,7 +51,9 @@ class Plant:
|
||||
from opendbc.car.honda.values import CAR
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
|
||||
self.planner = LongitudinalPlanner(CarInterface.get_non_essential_params(CAR.HONDA_CIVIC), init_v=self.speed)
|
||||
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, CAR.HONDA_CIVIC)
|
||||
self.planner = LongitudinalPlanner(CP, CP_SP, init_v=self.speed)
|
||||
|
||||
@property
|
||||
def current_time(self):
|
||||
|
||||
@@ -32,11 +32,11 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
|
||||
experimentalLongitudinalToggle = new ParamControl(
|
||||
"AlphaLongitudinalEnabled",
|
||||
tr("openpilot Longitudinal Control (Alpha)"),
|
||||
tr("sunnypilot Longitudinal Control (Alpha)"),
|
||||
QString("<b>%1</b><br><br>%2")
|
||||
.arg(tr("WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB)."))
|
||||
.arg(tr("On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. "
|
||||
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.")),
|
||||
.arg(tr("WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB)."))
|
||||
.arg(tr("On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. "
|
||||
"Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha.")),
|
||||
""
|
||||
);
|
||||
experimentalLongitudinalToggle->setConfirmation(true, false);
|
||||
|
||||
@@ -12,7 +12,7 @@ public:
|
||||
explicit DeveloperPanel(SettingsWindow *parent);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
private:
|
||||
protected:
|
||||
Params params;
|
||||
ParamControl* adbToggle;
|
||||
ParamControl* joystickToggle;
|
||||
|
||||
@@ -33,13 +33,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
"../assets/icons/experimental_white.svg",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"DynamicExperimentalControl",
|
||||
tr("Enable Dynamic Experimental Control"),
|
||||
tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"DisengageOnAccelerator",
|
||||
tr("Disengage on Accelerator Pedal"),
|
||||
@@ -195,7 +188,7 @@ void TogglesPanel::updateToggles() {
|
||||
const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.");
|
||||
|
||||
QString long_desc = unavailable + " " + \
|
||||
tr("openpilot longitudinal control may come in a future update.");
|
||||
tr("sunnypilot longitudinal control may come in a future update.");
|
||||
if (CP.getAlphaLongitudinalAvailable()) {
|
||||
if (is_release) {
|
||||
long_desc = unavailable + " " + tr("An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches.");
|
||||
|
||||
@@ -118,7 +118,7 @@ void AnnotatedCameraWidget::paintGL() {
|
||||
} else if (v_ego > 15) {
|
||||
wide_cam_requested = false;
|
||||
}
|
||||
wide_cam_requested = wide_cam_requested && sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
|
||||
// wide_cam_requested = wide_cam_requested && sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
|
||||
}
|
||||
CameraWidget::setStreamType(wide_cam_requested ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD);
|
||||
CameraWidget::setFrameId(sm["modelV2"].getModelV2().getFrameId());
|
||||
|
||||
@@ -22,7 +22,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
|
||||
update_model(model, lead_one);
|
||||
drawLaneLines(painter);
|
||||
drawPath(painter, model, surface_rect);
|
||||
drawPath(painter, model, surface_rect.height());
|
||||
|
||||
if (longitudinal_control && sm.alive("radarState")) {
|
||||
update_leads(radar_state, model.getPosition());
|
||||
@@ -173,7 +173,6 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
|
||||
(1 - t) * start.alphaF() + t * end.alphaF());
|
||||
}
|
||||
|
||||
|
||||
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &vd, const QRect &surface_rect) {
|
||||
const float speedBuff = 10.;
|
||||
|
||||
@@ -39,9 +39,6 @@ protected:
|
||||
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
|
||||
void drawLaneLines(QPainter &painter);
|
||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height);
|
||||
virtual void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) {;
|
||||
drawPath(painter, model, surface_rect.height());
|
||||
}
|
||||
void updatePathGradient(QLinearGradient &bg);
|
||||
QColor blendColors(const QColor &start, const QColor &end, float t);
|
||||
|
||||
@@ -58,5 +55,4 @@ protected:
|
||||
QPointF lead_vertices[2] = {};
|
||||
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
|
||||
QRectF clip_region;
|
||||
|
||||
};
|
||||
|
||||
@@ -11,17 +11,18 @@ WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) {
|
||||
main_layout->setContentsMargins(56, 40, 56, 40);
|
||||
main_layout->setSpacing(42);
|
||||
|
||||
QLabel *title = new QLabel(tr("<span style='font-family: \"Noto Color Emoji\";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span>"));
|
||||
title->setStyleSheet("font-size: 64px; font-weight: 500;");
|
||||
community_popup = new SunnylinkCommunityPopup(this);
|
||||
QLabel *title = new QLabel(tr("sunnypilot Community"));
|
||||
title->setStyleSheet("font-size: 56px; font-weight: 500;");
|
||||
main_layout->addWidget(title);
|
||||
|
||||
QLabel *desc = new QLabel(tr("Maximize your training data uploads to improve openpilot's driving models."));
|
||||
QLabel *desc = new QLabel(tr("Need help or have ideas?<br><b>Join</b> our community now!"));
|
||||
desc->setStyleSheet("font-size: 40px; font-weight: 400;");
|
||||
desc->setWordWrap(true);
|
||||
main_layout->addWidget(desc);
|
||||
|
||||
QPushButton *settings_btn = new QPushButton(tr("Open"));
|
||||
connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(1, "FirehosePanel"); });
|
||||
QPushButton *settings_btn = new QPushButton(tr("Learn More"));
|
||||
connect(settings_btn, &QPushButton::clicked, [=]() { community_popup->exec(); });
|
||||
settings_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
font-size: 48px;
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
#include <QFrame>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h"
|
||||
|
||||
class WiFiPromptWidget : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WiFiPromptWidget(QWidget* parent = 0);
|
||||
|
||||
private:
|
||||
SunnylinkCommunityPopup *community_popup;
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import time
|
||||
import wave
|
||||
|
||||
|
||||
from cereal import car, messaging
|
||||
from cereal import car, messaging, custom
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
@@ -26,8 +26,15 @@ AMBIENT_DB = 30 # DB where MIN_VOLUME is applied
|
||||
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
|
||||
|
||||
|
||||
sound_list_sp: dict[int, tuple[str, int | None, float]] = {
|
||||
# AudibleAlertSP, file name, play count (none for infinite)
|
||||
AudibleAlertSP.promptSingleLow: ("prompt_single_low.wav", 1, MAX_VOLUME),
|
||||
AudibleAlertSP.promptSingleHigh: ("prompt_single_high.wav", 1, MAX_VOLUME),
|
||||
}
|
||||
|
||||
sound_list: dict[int, tuple[str, int | None, float]] = {
|
||||
# AudibleAlert, file name, play count (none for infinite)
|
||||
AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME),
|
||||
@@ -40,6 +47,8 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
|
||||
|
||||
AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME),
|
||||
|
||||
**sound_list_sp,
|
||||
}
|
||||
|
||||
def check_selfdrive_timeout_alert(sm):
|
||||
|
||||
@@ -35,6 +35,7 @@ qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/software_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink/community_widget.cc",
|
||||
"sunnypilot/qt/offroad/settings/trips_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/vehicle_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/visuals_panel.cc",
|
||||
|
||||
@@ -60,7 +60,7 @@ DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(pare
|
||||
|
||||
void DeveloperPanelSP::updateToggles(bool offroad) {
|
||||
bool disable_updates = params.getBool("DisableUpdates");
|
||||
bool is_release = params.getBool("IsReleaseBranch");
|
||||
bool is_release = params.getBool("IsReleaseBranch") || params.getBool("IsReleaseSpBranch");
|
||||
bool is_tested = params.getBool("IsTestedBranch");
|
||||
bool is_development = params.getBool("IsDevelopmentBranch");
|
||||
|
||||
@@ -79,6 +79,9 @@ void DeveloperPanelSP::updateToggles(bool offroad) {
|
||||
enableGithubRunner->setVisible(!is_release);
|
||||
errorLogBtn->setVisible(!is_release);
|
||||
showAdvancedControls->setEnabled(true);
|
||||
|
||||
joystickToggle->setVisible(!is_release);
|
||||
longManeuverToggle->setVisible(!is_release);
|
||||
}
|
||||
|
||||
void DeveloperPanelSP::showEvent(QShowEvent *event) {
|
||||
|
||||
@@ -29,7 +29,7 @@ OnroadScreenBrightnessControl::OnroadScreenBrightnessControl(const QString ¶
|
||||
"Onroad Brightness",
|
||||
"",
|
||||
"",
|
||||
{0, 100}, 10, true);
|
||||
{0, 90}, 10, true);
|
||||
|
||||
connect(onroadScreenOffTimer, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);
|
||||
connect(onroadScreenBrightness, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);
|
||||
|
||||
@@ -13,9 +13,9 @@ enum class SpeedLimitOffsetType {
|
||||
};
|
||||
|
||||
inline const QString SpeedLimitOffsetTypeTexts[]{
|
||||
QObject::tr("None"),
|
||||
QObject::tr("Fixed"),
|
||||
QObject::tr("Percent"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "None"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Fixed"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Percent"),
|
||||
};
|
||||
|
||||
enum class SpeedLimitSourcePolicy {
|
||||
@@ -27,11 +27,11 @@ enum class SpeedLimitSourcePolicy {
|
||||
};
|
||||
|
||||
inline const QString SpeedLimitSourcePolicyTexts[]{
|
||||
QObject::tr("Car\nOnly"),
|
||||
QObject::tr("Map\nOnly"),
|
||||
QObject::tr("Car\nFirst"),
|
||||
QObject::tr("Map\nFirst"),
|
||||
QObject::tr("Combined\nData")
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nOnly"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nOnly"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nFirst"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nFirst"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Combined\nData")
|
||||
};
|
||||
|
||||
enum class SpeedLimitMode {
|
||||
@@ -42,8 +42,8 @@ enum class SpeedLimitMode {
|
||||
};
|
||||
|
||||
inline const QString SpeedLimitModeTexts[]{
|
||||
QObject::tr("Off"),
|
||||
QObject::tr("Information"),
|
||||
QObject::tr("Warning"),
|
||||
QObject::tr("Assist"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Off"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Information"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Warning"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Assist"),
|
||||
};
|
||||
|
||||
@@ -23,11 +23,11 @@ SpeedLimitPolicy::SpeedLimitPolicy(QWidget *parent) : QWidget(parent) {
|
||||
ListWidgetSP *list = new ListWidgetSP(this);
|
||||
|
||||
std::vector<QString> speed_limit_policy_texts{
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_ONLY)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_ONLY)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_FIRST)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_FIRST)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::COMBINED)]
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_ONLY)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_ONLY)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_FIRST)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_FIRST)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::COMBINED)].toStdString().c_str())
|
||||
};
|
||||
speed_limit_policy = new ButtonParamControlSP(
|
||||
"SpeedLimitPolicy",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
|
||||
SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent) {
|
||||
subPanelFrame = new QFrame();
|
||||
QVBoxLayout *subPanelLayout = new QVBoxLayout(subPanelFrame);
|
||||
@@ -25,10 +27,10 @@ SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent)
|
||||
speedLimitPolicyScreen = new SpeedLimitPolicy(this);
|
||||
|
||||
std::vector<QString> speed_limit_mode_texts{
|
||||
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::OFF)],
|
||||
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::INFORMATION)],
|
||||
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::WARNING)],
|
||||
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::ASSIST)],
|
||||
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::OFF)].toStdString().c_str()),
|
||||
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::INFORMATION)].toStdString().c_str()),
|
||||
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::WARNING)].toStdString().c_str()),
|
||||
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::ASSIST)].toStdString().c_str())
|
||||
};
|
||||
speed_limit_mode_settings = new ButtonParamControlSP(
|
||||
"SpeedLimitMode",
|
||||
@@ -64,9 +66,9 @@ SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent)
|
||||
QVBoxLayout *offsetLayout = new QVBoxLayout(offsetFrame);
|
||||
|
||||
std::vector<QString> speed_limit_offset_texts{
|
||||
SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::NONE)],
|
||||
SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::FIXED)],
|
||||
SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::PERCENT)]
|
||||
tr(SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::NONE)].toStdString().c_str()),
|
||||
tr(SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::FIXED)].toStdString().c_str()),
|
||||
tr(SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::PERCENT)].toStdString().c_str())
|
||||
};
|
||||
speed_limit_offset_settings = new ButtonParamControlSP(
|
||||
"SpeedLimitOffsetType",
|
||||
@@ -103,13 +105,13 @@ SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent)
|
||||
}
|
||||
|
||||
void SpeedLimitSettings::refresh() {
|
||||
bool is_release = params.getBool("IsReleaseSpBranch");
|
||||
bool is_metric_param = params.getBool("IsMetric");
|
||||
SpeedLimitMode speed_limit_mode_param = static_cast<SpeedLimitMode>(std::atoi(params.get("SpeedLimitMode").c_str()));
|
||||
SpeedLimitOffsetType offset_type_param = static_cast<SpeedLimitOffsetType>(std::atoi(params.get("SpeedLimitOffsetType").c_str()));
|
||||
QString offsetLabel = QString::fromStdString(params.get("SpeedLimitValueOffset"));
|
||||
|
||||
bool has_longitudinal_control;
|
||||
bool intelligent_cruise_button_management_available;
|
||||
bool sla_available;
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
auto cp_sp_bytes = params.get("CarParamsSPPersistent");
|
||||
if (!cp_bytes.empty() && !cp_sp_bytes.empty()) {
|
||||
@@ -120,11 +122,24 @@ void SpeedLimitSettings::refresh() {
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
cereal::CarParamsSP::Reader CP_SP = cmsg_sp.getRoot<cereal::CarParamsSP>();
|
||||
|
||||
has_longitudinal_control = hasLongitudinalControl(CP);
|
||||
intelligent_cruise_button_management_available = CP_SP.getIntelligentCruiseButtonManagementAvailable();
|
||||
bool has_longitudinal_control = hasLongitudinalControl(CP);
|
||||
bool has_icbm = hasIntelligentCruiseButtonManagement(CP_SP);
|
||||
|
||||
/*
|
||||
* Speed Limit Assist is available when:
|
||||
* - has_longitudinal_control or has_icbm, and
|
||||
* - is not a release branch or not a disallowed brand, and
|
||||
* - is not always disallowed
|
||||
*/
|
||||
bool sla_disallow_in_release = CP.getBrand() == "tesla" && is_release;
|
||||
bool sla_always_disallow = CP.getBrand() == "rivian";
|
||||
sla_available = (has_longitudinal_control || has_icbm) && !sla_disallow_in_release && !sla_always_disallow;
|
||||
|
||||
if (!sla_available && speed_limit_mode_param == SpeedLimitMode::ASSIST) {
|
||||
params.put("SpeedLimitMode", std::to_string(static_cast<int>(SpeedLimitMode::WARNING)));
|
||||
}
|
||||
} else {
|
||||
has_longitudinal_control = false;
|
||||
intelligent_cruise_button_management_available = false;
|
||||
sla_available = false;
|
||||
}
|
||||
|
||||
speed_limit_mode_settings->setDescription(modeDescription(speed_limit_mode_param));
|
||||
@@ -144,13 +159,14 @@ void SpeedLimitSettings::refresh() {
|
||||
speed_limit_offset->showDescription();
|
||||
}
|
||||
|
||||
if (has_longitudinal_control || intelligent_cruise_button_management_available) {
|
||||
if (sla_available) {
|
||||
speed_limit_mode_settings->setEnableSelectedButtons(true, convertSpeedLimitModeValues(getSpeedLimitModeValues()));
|
||||
} else {
|
||||
speed_limit_mode_settings->setEnableSelectedButtons(true, convertSpeedLimitModeValues(
|
||||
{SpeedLimitMode::OFF,SpeedLimitMode::INFORMATION, SpeedLimitMode::WARNING}));
|
||||
{SpeedLimitMode::OFF, SpeedLimitMode::INFORMATION, SpeedLimitMode::WARNING}));
|
||||
}
|
||||
|
||||
speed_limit_mode_settings->refresh();
|
||||
speed_limit_mode_settings->showDescription();
|
||||
speed_limit_offset->showDescription();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ private:
|
||||
SpeedLimitPolicy *speedLimitPolicyScreen;
|
||||
ButtonParamControlSP *speed_limit_offset_settings;
|
||||
OptionControlSP *speed_limit_offset;
|
||||
bool icbm_available = false;
|
||||
|
||||
static QString offsetDescription(SpeedLimitOffsetType type = SpeedLimitOffsetType::NONE) {
|
||||
QString none_str = tr("⦿ None: No Offset");
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
|
||||
LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
|
||||
setStyleSheet(R"(
|
||||
#back_btn {
|
||||
@@ -36,13 +38,24 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
|
||||
intelligentCruiseButtonManagement = new ParamControlSP(
|
||||
"IntelligentCruiseButtonManagement",
|
||||
tr("Intelligent Cruise Button Management (ICBM) (Alpha)"),
|
||||
tr("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control."),
|
||||
"",
|
||||
"",
|
||||
this
|
||||
);
|
||||
intelligentCruiseButtonManagement->setConfirmation(true, false);
|
||||
QObject::connect(intelligentCruiseButtonManagement, &ParamControlSP::toggleFlipped, this, [=](bool) {
|
||||
refresh(offroad);
|
||||
});
|
||||
list->addItem(intelligentCruiseButtonManagement);
|
||||
|
||||
dynamicExperimentalControl = new ParamControlSP(
|
||||
"DynamicExperimentalControl",
|
||||
tr("Dynamic Experimental Control (DEC)"),
|
||||
tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."),
|
||||
"",
|
||||
this
|
||||
);
|
||||
list->addItem(dynamicExperimentalControl);
|
||||
|
||||
SmartCruiseControlVision = new ParamControl(
|
||||
"SmartCruiseControlVision",
|
||||
tr("Smart Cruise Control - Vision"),
|
||||
@@ -91,6 +104,8 @@ void LongitudinalPanel::hideEvent(QHideEvent *event) {
|
||||
}
|
||||
|
||||
void LongitudinalPanel::refresh(bool _offroad) {
|
||||
const QString icbm_description = tr("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control.");
|
||||
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
auto cp_sp_bytes = params.get("CarParamsSPPersistent");
|
||||
if (!cp_bytes.empty() && !cp_sp_bytes.empty()) {
|
||||
@@ -103,15 +118,61 @@ void LongitudinalPanel::refresh(bool _offroad) {
|
||||
|
||||
has_longitudinal_control = hasLongitudinalControl(CP);
|
||||
is_pcm_cruise = CP.getPcmCruise();
|
||||
intelligent_cruise_button_management_available = CP_SP.getIntelligentCruiseButtonManagementAvailable();
|
||||
has_icbm = hasIntelligentCruiseButtonManagement(CP_SP);
|
||||
|
||||
if (CP_SP.getIntelligentCruiseButtonManagementAvailable() && !has_longitudinal_control) {
|
||||
intelligentCruiseButtonManagement->setEnabled(offroad);
|
||||
intelligentCruiseButtonManagement->setDescription(icbm_description);
|
||||
} else {
|
||||
params.remove("IntelligentCruiseButtonManagement");
|
||||
intelligentCruiseButtonManagement->setEnabled(false);
|
||||
|
||||
const QString icbm_unavaialble = tr("Intelligent Cruise Button Management is currently unavailable on this platform.");
|
||||
|
||||
QString long_desc = icbm_unavaialble;
|
||||
if (has_longitudinal_control) {
|
||||
if (CP.getAlphaLongitudinalAvailable()) {
|
||||
long_desc = icbm_unavaialble + " " + tr("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.");
|
||||
} else {
|
||||
long_desc = icbm_unavaialble + " " + tr("sunnypilot Longitudinal Control is the default longitudinal control for this platform.");
|
||||
}
|
||||
}
|
||||
|
||||
intelligentCruiseButtonManagement->setDescription("<b>" + long_desc + "</b><br><br>" + icbm_description);
|
||||
intelligentCruiseButtonManagement->showDescription();
|
||||
}
|
||||
|
||||
if (has_longitudinal_control || has_icbm) {
|
||||
// enable Custom ACC Increments when long is available and is not PCM cruise
|
||||
customAccIncrement->setEnabled(((has_longitudinal_control && !is_pcm_cruise) || has_icbm) && offroad);
|
||||
dynamicExperimentalControl->setEnabled(has_longitudinal_control);
|
||||
SmartCruiseControlVision->setEnabled(true);
|
||||
SmartCruiseControlMap->setEnabled(true);
|
||||
} else {
|
||||
params.remove("CustomAccIncrementsEnabled");
|
||||
params.remove("DynamicExperimentalControl");
|
||||
params.remove("SmartCruiseControlVision");
|
||||
params.remove("SmartCruiseControlMap");
|
||||
customAccIncrement->setEnabled(false);
|
||||
dynamicExperimentalControl->setEnabled(false);
|
||||
SmartCruiseControlVision->setEnabled(false);
|
||||
SmartCruiseControlMap->setEnabled(false);
|
||||
}
|
||||
|
||||
intelligentCruiseButtonManagement->refresh();
|
||||
customAccIncrement->refresh();
|
||||
dynamicExperimentalControl->refresh();
|
||||
SmartCruiseControlVision->refresh();
|
||||
SmartCruiseControlMap->refresh();
|
||||
} else {
|
||||
has_longitudinal_control = false;
|
||||
is_pcm_cruise = false;
|
||||
intelligent_cruise_button_management_available = false;
|
||||
has_icbm = false;
|
||||
intelligentCruiseButtonManagement->setDescription("<b>" + tr("Start the vehicle to check vehicle compatibility.") + "</br><b><b>" + icbm_description);
|
||||
}
|
||||
|
||||
QString accEnabledDescription = tr("Enable custom Short & Long press increments for cruise speed increase/decrease.");
|
||||
QString accNoLongDescription = tr("This feature can only be used with openpilot longitudinal control enabled.");
|
||||
QString accNoLongDescription = tr("This feature can only be used with sunnypilot longitudinal control enabled.");
|
||||
QString accPcmCruiseDisabledDescription = tr("This feature is not supported on this platform due to vehicle limitations.");
|
||||
QString onroadOnlyDescription = tr("Start the vehicle to check vehicle compatibility.");
|
||||
|
||||
@@ -119,33 +180,19 @@ void LongitudinalPanel::refresh(bool _offroad) {
|
||||
customAccIncrement->setDescription(onroadOnlyDescription);
|
||||
customAccIncrement->showDescription();
|
||||
} else {
|
||||
if (has_longitudinal_control || intelligent_cruise_button_management_available) {
|
||||
if (is_pcm_cruise) {
|
||||
if (has_longitudinal_control || has_icbm) {
|
||||
if (has_longitudinal_control && is_pcm_cruise) {
|
||||
customAccIncrement->setDescription(accPcmCruiseDisabledDescription);
|
||||
customAccIncrement->showDescription();
|
||||
} else {
|
||||
customAccIncrement->setDescription(accEnabledDescription);
|
||||
}
|
||||
} else {
|
||||
params.remove("CustomAccIncrementsEnabled");
|
||||
customAccIncrement->toggleFlipped(false);
|
||||
customAccIncrement->setDescription(accNoLongDescription);
|
||||
customAccIncrement->showDescription();
|
||||
params.remove("IntelligentCruiseButtonManagement");
|
||||
intelligentCruiseButtonManagement->toggleFlipped(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool icbm_allowed = intelligent_cruise_button_management_available && !has_longitudinal_control;
|
||||
intelligentCruiseButtonManagement->setEnabled(icbm_allowed && offroad);
|
||||
|
||||
// enable toggle when long is available and is not PCM cruise
|
||||
bool cai_allowed = (has_longitudinal_control && !is_pcm_cruise) || icbm_allowed;
|
||||
customAccIncrement->setEnabled(cai_allowed && !offroad);
|
||||
customAccIncrement->refresh();
|
||||
|
||||
SmartCruiseControlVision->setEnabled(has_longitudinal_control || icbm_allowed);
|
||||
SmartCruiseControlMap->setEnabled(has_longitudinal_control || icbm_allowed);
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ private:
|
||||
Params params;
|
||||
bool has_longitudinal_control = false;
|
||||
bool is_pcm_cruise = false;
|
||||
bool intelligent_cruise_button_management_available = false;;
|
||||
bool has_icbm = false;
|
||||
bool offroad = false;
|
||||
|
||||
QStackedLayout *main_layout = nullptr;
|
||||
@@ -35,6 +35,7 @@ private:
|
||||
ParamControl *SmartCruiseControlVision;
|
||||
ParamControl *SmartCruiseControlMap;
|
||||
ParamControl *intelligentCruiseButtonManagement = nullptr;
|
||||
ParamControl *dynamicExperimentalControl = nullptr;
|
||||
SpeedLimitSettings *speedLimitScreen;
|
||||
PushButtonSP *speedLimitSettings;
|
||||
};
|
||||
|
||||
@@ -310,9 +310,8 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
||||
QList<TreeNode> sortedModels;
|
||||
QSet<QString> modelFolders;
|
||||
QRegularExpression re("\\(([^)]*)\\)[^(]*$");
|
||||
const auto bundles = model_manager.getAvailableBundles();
|
||||
|
||||
for (const auto &bundle : bundles) {
|
||||
for (const auto &bundle : model_manager.getAvailableBundles()) {
|
||||
auto overrides = bundle.getOverrides();
|
||||
QString folder;
|
||||
for (const auto &override : overrides) {
|
||||
@@ -392,7 +391,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
||||
showResetParamsDialog();
|
||||
} else {
|
||||
// Find selected bundle and initiate download
|
||||
for (const auto &bundle: bundles) {
|
||||
for (const auto &bundle: model_manager.getAvailableBundles()) {
|
||||
if (QString::fromStdString(bundle.getRef()) == selectedBundleRef) {
|
||||
params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex()));
|
||||
if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2025-, sunnypilot contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/community_widget.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
|
||||
using qrcodegen::QrCode;
|
||||
|
||||
// --- SunnylinkCommunityQRWidget ---
|
||||
|
||||
SunnylinkCommunityQRWidget::SunnylinkCommunityQRWidget(QWidget* parent)
|
||||
: QWidget(parent) {}
|
||||
|
||||
void SunnylinkCommunityQRWidget::showEvent(QShowEvent *event) {
|
||||
updateQrCode(SUNNYLINK_COMMUNITY_URL);
|
||||
update();
|
||||
}
|
||||
|
||||
void SunnylinkCommunityQRWidget::updateQrCode(const QString &text) {
|
||||
QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW);
|
||||
qint32 sz = qr.getSize();
|
||||
QImage im(sz, sz, QImage::Format_RGB32);
|
||||
|
||||
QRgb black = qRgb(0, 0, 0);
|
||||
QRgb white = qRgb(255, 255, 255);
|
||||
for (int y = 0; y < sz; y++) {
|
||||
for (int x = 0; x < sz; x++) {
|
||||
im.setPixel(x, y, qr.getModule(x, y) ? black : white);
|
||||
}
|
||||
}
|
||||
|
||||
int final_sz = ((width() / sz) - 1) * sz;
|
||||
img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly);
|
||||
}
|
||||
|
||||
void SunnylinkCommunityQRWidget::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), Qt::white);
|
||||
|
||||
if (!img.isNull()) {
|
||||
QSize s = (size() - img.size()) / 2;
|
||||
p.drawPixmap(s.width(), s.height(), img);
|
||||
}
|
||||
}
|
||||
|
||||
// --- SunnylinkCommunityPopup ---
|
||||
|
||||
QStringList SunnylinkCommunityPopup::getInstructions() {
|
||||
QStringList instructions;
|
||||
instructions << tr("Scan the QR code and join us!");
|
||||
return instructions;
|
||||
}
|
||||
|
||||
SunnylinkCommunityPopup::SunnylinkCommunityPopup(QWidget* parent)
|
||||
: DialogBase(parent) {
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(0);
|
||||
|
||||
// Solarized Light base3 background
|
||||
setStyleSheet("SunnylinkCommunityPopup { background-color: #FDF6E3; }");
|
||||
|
||||
// Header spanning full width
|
||||
auto headerWidget = new QWidget(this);
|
||||
auto headerLayout = new QHBoxLayout(headerWidget);
|
||||
headerLayout->setContentsMargins(85, 50, 85, 30);
|
||||
headerLayout->setSpacing(30);
|
||||
|
||||
auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this);
|
||||
close->setIconSize(QSize(80, 80));
|
||||
close->setStyleSheet("border: none;");
|
||||
connect(close, &QPushButton::clicked, this, &QDialog::reject);
|
||||
headerLayout->addWidget(close, 0, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
const auto title = new QLabel(tr("Join the sunnypilot Community Forum"), this);
|
||||
// Solarized base02 for text
|
||||
title->setStyleSheet("font-size: 65px; color: #073642;");
|
||||
title->setWordWrap(false);
|
||||
title->setAlignment(Qt::AlignCenter);
|
||||
headerLayout->addWidget(title, 1);
|
||||
|
||||
// Spacer to balance the close button on the right
|
||||
auto spacer = new QWidget(this);
|
||||
spacer->setFixedSize(80, 80);
|
||||
headerLayout->addWidget(spacer, 0);
|
||||
|
||||
mainLayout->addWidget(headerWidget);
|
||||
|
||||
// Two-column content layout
|
||||
auto contentLayout = new QHBoxLayout();
|
||||
contentLayout->setContentsMargins(0, 0, 0, 0);
|
||||
contentLayout->setSpacing(0);
|
||||
mainLayout->addLayout(contentLayout, 66);
|
||||
|
||||
// Left side: description
|
||||
auto leftLayout = new QVBoxLayout();
|
||||
leftLayout->setContentsMargins(85, 40, 50, 70);
|
||||
leftLayout->setSpacing(35);
|
||||
contentLayout->addLayout(leftLayout, 40);
|
||||
|
||||
// Hype / intro paragraph
|
||||
const auto desc = new QLabel(tr(
|
||||
"We're excited to announce our <b>sunnypilot Community Forum</b><br><br>"
|
||||
"Over the years, Discord just hasn't scaled well for our growing community.<br>"
|
||||
"It's noisy, unsearchable, and great discussions disappear too easily.<br>"
|
||||
"Our new community forum aims to fix that by making it easier to <b>find answers, share ideas, track feedback, report bugs, help newcomers</b> and more!<br><br>"
|
||||
"<b>Here's what's waiting for you:</b><br>"
|
||||
"• Fully <b>indexable</b> and discoverable through search engines 🔎<br>"
|
||||
"• <b>AI-powered</b>🤖 topic and chat summaries, spam detection, and more<br>"
|
||||
"• A <b>trust-level system</b>✅ that rewards meaningful contributions<br>"
|
||||
"• Designed to work <b>on your own time</b>.🧘<br><br>"
|
||||
"Scan the QR code on the right and join the discussion!"
|
||||
), this);
|
||||
// Solarized base01 for body text
|
||||
desc->setStyleSheet("font-size: 40px; color: #586E75;");
|
||||
desc->setWordWrap(true);
|
||||
leftLayout->addWidget(desc);
|
||||
|
||||
leftLayout->addStretch();
|
||||
|
||||
// Right side: QR code and instructions
|
||||
auto rightLayout = new QVBoxLayout();
|
||||
rightLayout->setContentsMargins(50, 40, 85, 70);
|
||||
rightLayout->setSpacing(40);
|
||||
contentLayout->addLayout(rightLayout, 1);
|
||||
|
||||
// QR code (smaller, fixed size)
|
||||
auto *qr = new SunnylinkCommunityQRWidget(this);
|
||||
qr->setFixedSize(500, 500);
|
||||
rightLayout->addStretch();
|
||||
rightLayout->addWidget(qr, 0, Qt::AlignCenter);
|
||||
rightLayout->addStretch();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025-, sunnypilot contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QrCode.hpp>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
const QString SUNNYLINK_COMMUNITY_URL = "https://community.sunnypilot.ai/sp-qr";
|
||||
|
||||
class SunnylinkCommunityQRWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SunnylinkCommunityQRWidget(QWidget* parent = nullptr);
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private:
|
||||
QPixmap img;
|
||||
void updateQrCode(const QString &text);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
};
|
||||
|
||||
// Popup widget
|
||||
class SunnylinkCommunityPopup : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SunnylinkCommunityPopup(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
static QStringList getInstructions();
|
||||
};
|
||||
@@ -79,11 +79,11 @@ QStringList SunnylinkSponsorPopup::getInstructions(bool sponsor_pair) {
|
||||
instructions << tr("Scan the QR code to login to your GitHub account")
|
||||
<< tr("Follow the prompts to complete the pairing process")
|
||||
<< tr("Re-enter the \"sunnylink\" panel to verify sponsorship status")
|
||||
<< tr("If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot");
|
||||
<< tr("If sponsorship status was not updated, please contact a moderator on our forum at https://community.sunnypilot.ai");
|
||||
} else {
|
||||
instructions << tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page")
|
||||
<< tr("Choose your sponsorship tier and confirm your support")
|
||||
<< tr("Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status");
|
||||
<< tr("Join our Community Forum at https://community.sunnypilot.ai and reach out to a moderator if you have issues");
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
QString sunnylinkUploaderDesc = tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)");
|
||||
sunnylinkUploaderEnabledBtn = new ParamControlSP(
|
||||
"EnableSunnylinkUploader",
|
||||
tr("[Don't use] Enable sunnylink uploader"),
|
||||
tr("Enable sunnylink uploader (infrastructure test)"),
|
||||
sunnylinkUploaderDesc,
|
||||
"", nullptr, true);
|
||||
list->addItem(sunnylinkUploaderEnabledBtn);
|
||||
@@ -290,7 +290,10 @@ void SunnylinkPanel::updatePanel() {
|
||||
pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled);
|
||||
pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired"));
|
||||
|
||||
sunnylinkUploaderEnabledBtn->setEnabled(max_current_sponsor_rule.roleTier == SponsorTier::Guardian && is_sunnylink_enabled);
|
||||
bool can_do_uploads = max_current_sponsor_rule.roleTier >= SponsorTier::Novice && is_sunnylink_enabled;
|
||||
sunnylinkUploaderEnabledBtn->setVisible(can_do_uploads);
|
||||
sunnylinkUploaderEnabledBtn->setEnabled(can_do_uploads);
|
||||
|
||||
|
||||
if (!is_sunnylink_enabled) {
|
||||
sunnylinkEnabledBtn->setValue("");
|
||||
|
||||
@@ -33,7 +33,7 @@ private:
|
||||
|
||||
static QString toggleDisableMsg(bool _offroad, bool _has_longitudinal_control) {
|
||||
if (!_has_longitudinal_control) {
|
||||
return tr("This feature can only be used with openpilot longitudinal control enabled.");
|
||||
return tr("This feature can only be used with sunnypilot longitudinal control enabled.");
|
||||
}
|
||||
|
||||
if (!_offroad) {
|
||||
@@ -57,7 +57,7 @@ private:
|
||||
}
|
||||
|
||||
return QString("%1<br><br>%2<br>%3<br>%4<br>")
|
||||
.arg(tr("Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control."))
|
||||
.arg(tr("Fine-tune your driving experience by adjusting acceleration smoothness with sunnypilot longitudinal control."))
|
||||
.arg(off_str)
|
||||
.arg(dynamic_str)
|
||||
.arg(predictive_str);
|
||||
|
||||
@@ -8,7 +8,52 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h"
|
||||
|
||||
SubaruSettings::SubaruSettings(QWidget *parent) : BrandSettingsInterface(parent) {
|
||||
stopAndGoToggle = new ParamControl("SubaruStopAndGo", tr("Stop and Go (Beta)"), "", "");
|
||||
stopAndGoToggle->setConfirmation(true, false);
|
||||
list->addItem(stopAndGoToggle);
|
||||
|
||||
stopAndGoManualParkingBrakeToggle = new ParamControl(
|
||||
"SubaruStopAndGoManualParkingBrake",
|
||||
tr("Stop and Go for Manual Parking Brake (Beta)"),
|
||||
"",
|
||||
""
|
||||
);
|
||||
stopAndGoManualParkingBrakeToggle->setConfirmation(true, false);
|
||||
list->addItem(stopAndGoManualParkingBrakeToggle);
|
||||
}
|
||||
|
||||
void SubaruSettings::updateSettings() {
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
|
||||
is_subaru = CP.getBrand() == "subaru";
|
||||
|
||||
if (is_subaru) {
|
||||
if (!(CP.getFlags() & (SUBARU_FLAG_GLOBAL_GEN2 | SUBARU_FLAG_HYBRID))) {
|
||||
has_stop_and_go = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
is_subaru = false;
|
||||
has_stop_and_go = false;
|
||||
}
|
||||
|
||||
bool stop_and_go_disabled = !offroad || !has_stop_and_go;
|
||||
QString stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc);
|
||||
QString stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc);
|
||||
if (stop_and_go_disabled) {
|
||||
stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc, stopAndGoDisabledMsg());
|
||||
stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc, stopAndGoDisabledMsg());
|
||||
}
|
||||
|
||||
stopAndGoToggle->setEnabled(has_stop_and_go);
|
||||
stopAndGoToggle->setDescription(stop_and_go_desc);
|
||||
stopAndGoToggle->showDescription();
|
||||
|
||||
stopAndGoManualParkingBrakeToggle->setEnabled(has_stop_and_go);
|
||||
stopAndGoManualParkingBrakeToggle->setDescription(stop_and_go_manual_parking_brake_desc);
|
||||
stopAndGoManualParkingBrakeToggle->showDescription();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
const int SUBARU_FLAG_GLOBAL_GEN2 = 4;
|
||||
const int SUBARU_FLAG_HYBRID = 32;
|
||||
|
||||
class SubaruSettings : public BrandSettingsInterface {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -23,4 +26,32 @@ public:
|
||||
|
||||
private:
|
||||
bool offroad = false;
|
||||
bool is_subaru;
|
||||
bool has_stop_and_go;
|
||||
|
||||
ParamControl* stopAndGoToggle;
|
||||
ParamControl* stopAndGoManualParkingBrakeToggle;
|
||||
|
||||
QString stopAndGoDesc = tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms.");
|
||||
QString stopAndGoManualParkingBrakeDesc = tr("Experimental feature to enable stop and go for Subaru Global models with manual handbrake. Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!");
|
||||
|
||||
QString stopAndGoDisabledMsg() const {
|
||||
if (is_subaru && !has_stop_and_go) {
|
||||
return tr("This feature is currently not available on this platform.");
|
||||
}
|
||||
|
||||
if (!is_subaru) {
|
||||
return tr("Start the car to check car compatibility.");
|
||||
}
|
||||
|
||||
if (!offroad) {
|
||||
return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.");
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
static QString stopAndGoDescriptionBuilder(const QString &base_description, const QString &custom_description = "") {
|
||||
return "<b>" + custom_description + "</b><br><br>" + base_description;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,41 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/tesla_settings.h"
|
||||
|
||||
TeslaSettings::TeslaSettings(QWidget *parent) : BrandSettingsInterface(parent) {
|
||||
constexpr int coopSteeringMinKmh = 23; // minimum speed for cooperative steering (enforced by Tesla firmware)
|
||||
constexpr int oemSteeringMinKmh = 48; // minimum speed for OEM lane departure avoidance (enforced by Tesla firmware)
|
||||
bool is_metric = params.getBool("IsMetric");
|
||||
QString unit = is_metric ? "km/h" : "mph";
|
||||
int display_value_coop;
|
||||
int display_value_oem;
|
||||
if (is_metric) {
|
||||
display_value_coop = coopSteeringMinKmh;
|
||||
display_value_oem = oemSteeringMinKmh;
|
||||
} else {
|
||||
display_value_coop = static_cast<int>(std::round(coopSteeringMinKmh * KM_TO_MILE));
|
||||
display_value_oem = static_cast<int>(std::round(oemSteeringMinKmh * KM_TO_MILE));
|
||||
}
|
||||
const QString coop_desc = QString("<b>%1</b><br><br>"
|
||||
"%2<br>"
|
||||
"%3<br>")
|
||||
.arg(tr("Warning: May experience steering oscillations below %5 %6 during turns, recommend disabling this feature if you experience these."))
|
||||
.arg(tr("Allows the driver to provide limited steering input while openpilot is engaged."))
|
||||
.arg(tr("Only works above %4 %6."))
|
||||
.arg(display_value_coop)
|
||||
.arg(display_value_oem)
|
||||
.arg(unit);
|
||||
|
||||
coopSteeringToggle = new ParamControlSP(
|
||||
"TeslaCoopSteering",
|
||||
tr("Cooperative Steering (Beta)"),
|
||||
coop_desc,
|
||||
"",
|
||||
this
|
||||
);
|
||||
list->addItem(coopSteeringToggle);
|
||||
coopSteeringToggle->showDescription();
|
||||
coopSteeringToggle->setConfirmation(true, false);
|
||||
}
|
||||
|
||||
void TeslaSettings::updateSettings() {
|
||||
coopSteeringToggle->setEnabled(offroad);
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ public:
|
||||
void updateSettings() override;
|
||||
|
||||
private:
|
||||
bool offroad = false;
|
||||
ParamControlSP *coopSteeringToggle = nullptr;
|
||||
};
|
||||
|
||||
@@ -119,7 +119,7 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
|
||||
// Visuals: Display Metrics below Chevron
|
||||
std::vector<QString> chevron_info_settings_texts{tr("Off"), tr("Distance"), tr("Speed"), tr("Time"), tr("All")};
|
||||
chevron_info_settings = new ButtonParamControlSP(
|
||||
"ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control)."),
|
||||
"ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with sunnypilot longitudinal control)."),
|
||||
"",
|
||||
chevron_info_settings_texts,
|
||||
200);
|
||||
@@ -159,8 +159,8 @@ void VisualsPanel::refreshLongitudinalStatus() {
|
||||
}
|
||||
|
||||
if (chevron_info_settings) {
|
||||
QString chevronEnabledDescription = tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control).");
|
||||
QString chevronNoLongDescription = tr("This feature requires openpilot longitudinal control to be available.");
|
||||
QString chevronEnabledDescription = tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with sunnypilot longitudinal control).");
|
||||
QString chevronNoLongDescription = tr("This feature requires sunnypilot longitudinal control to be available.");
|
||||
|
||||
if (has_longitudinal_control) {
|
||||
chevron_info_settings->setDescription(chevronEnabledDescription);
|
||||
|
||||
@@ -12,43 +12,65 @@
|
||||
|
||||
|
||||
HudRendererSP::HudRendererSP() {
|
||||
plus_arrow_up_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_plus_arrow_up", {105, 105});
|
||||
minus_arrow_down_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_minus_arrow_down", {105, 105});
|
||||
plus_arrow_up_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_plus_arrow_up", {90, 90});
|
||||
minus_arrow_down_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_minus_arrow_down", {90, 90});
|
||||
|
||||
int small_max = e2e_alert_small * 2 - 40;
|
||||
int large_max = e2e_alert_large * 2 - 40;
|
||||
green_light_alert_small_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/green_light.png", {small_max, small_max});
|
||||
green_light_alert_large_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/green_light.png", {large_max, large_max});
|
||||
lead_depart_alert_small_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/lead_depart.png", {small_max, small_max});
|
||||
lead_depart_alert_large_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/lead_depart.png", {large_max, large_max});
|
||||
int size = e2e_alert_size * 2 - 40;
|
||||
green_light_alert_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/green_light.png", {size, size});
|
||||
lead_depart_alert_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/lead_depart.png", {size, size});
|
||||
}
|
||||
|
||||
void HudRendererSP::updateState(const UIState &s) {
|
||||
HudRenderer::updateState(s);
|
||||
|
||||
float speedConv = is_metric ? MS_TO_KPH : MS_TO_MPH;
|
||||
devUiInfo = s.scene.dev_ui_info;
|
||||
roadName = s.scene.road_name;
|
||||
showTurnSignals = s.scene.turn_signals;
|
||||
speedLimitMode = static_cast<SpeedLimitMode>(s.scene.speed_limit_mode);
|
||||
speedUnit = is_metric ? tr("km/h") : tr("mph");
|
||||
standstillTimer = s.scene.standstill_timer;
|
||||
|
||||
const SubMaster &sm = *(s.sm);
|
||||
const auto cs = sm["controlsState"].getControlsState();
|
||||
const auto car_state = sm["carState"].getCarState();
|
||||
const auto car_control = sm["carControl"].getCarControl();
|
||||
const auto radar_state = sm["radarState"].getRadarState();
|
||||
const auto is_gps_location_external = sm.rcv_frame("gpsLocationExternal") > 1;
|
||||
const auto gpsLocation = is_gps_location_external ? sm["gpsLocationExternal"].getGpsLocationExternal() : sm["gpsLocation"].getGpsLocation();
|
||||
const char *gps_source = is_gps_location_external ? "gpsLocationExternal" : "gpsLocation";
|
||||
const auto gpsLocation = is_gps_location_external ? sm[gps_source].getGpsLocationExternal() : sm[gps_source].getGpsLocation();
|
||||
const auto ltp = sm["liveTorqueParameters"].getLiveTorqueParameters();
|
||||
const auto car_params = sm["carParams"].getCarParams();
|
||||
const auto car_params_sp = sm["carParamsSP"].getCarParamsSP();
|
||||
const auto lp_sp = sm["longitudinalPlanSP"].getLongitudinalPlanSP();
|
||||
const auto lmd = sm["liveMapDataSP"].getLiveMapDataSP();
|
||||
|
||||
float speedConv = is_metric ? MS_TO_KPH : MS_TO_MPH;
|
||||
speedLimit = lp_sp.getSpeedLimit().getResolver().getSpeedLimit() * speedConv;
|
||||
speedLimitLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLast() * speedConv;
|
||||
speedLimitOffset = lp_sp.getSpeedLimit().getResolver().getSpeedLimitOffset() * speedConv;
|
||||
speedLimitValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitValid();
|
||||
speedLimitLastValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLastValid();
|
||||
speedLimitFinalLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitFinalLast() * speedConv;
|
||||
speedLimitMode = static_cast<SpeedLimitMode>(s.scene.speed_limit_mode);
|
||||
speedLimitAssistState = lp_sp.getSpeedLimit().getAssist().getState();
|
||||
speedLimitAssistActive = lp_sp.getSpeedLimit().getAssist().getActive();
|
||||
roadName = s.scene.road_name;
|
||||
if (sm.updated("carParams")) {
|
||||
steerControlType = car_params.getSteerControlType();
|
||||
}
|
||||
|
||||
if (sm.updated("carParamsSP")) {
|
||||
pcmCruiseSpeed = car_params_sp.getPcmCruiseSpeed();
|
||||
}
|
||||
|
||||
if (sm.updated("longitudinalPlanSP")) {
|
||||
speedLimit = lp_sp.getSpeedLimit().getResolver().getSpeedLimit() * speedConv;
|
||||
speedLimitLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLast() * speedConv;
|
||||
speedLimitOffset = lp_sp.getSpeedLimit().getResolver().getSpeedLimitOffset() * speedConv;
|
||||
speedLimitValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitValid();
|
||||
speedLimitLastValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLastValid();
|
||||
speedLimitFinalLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitFinalLast() * speedConv;
|
||||
speedLimitSource = lp_sp.getSpeedLimit().getResolver().getSource();
|
||||
speedLimitAssistState = lp_sp.getSpeedLimit().getAssist().getState();
|
||||
speedLimitAssistActive = lp_sp.getSpeedLimit().getAssist().getActive();
|
||||
smartCruiseControlVisionEnabled = lp_sp.getSmartCruiseControl().getVision().getEnabled();
|
||||
smartCruiseControlVisionActive = lp_sp.getSmartCruiseControl().getVision().getActive();
|
||||
smartCruiseControlMapEnabled = lp_sp.getSmartCruiseControl().getMap().getEnabled();
|
||||
smartCruiseControlMapActive = lp_sp.getSmartCruiseControl().getMap().getActive();
|
||||
}
|
||||
greenLightAlert = lp_sp.getE2eAlerts().getGreenLightAlert();
|
||||
leadDepartAlert = lp_sp.getE2eAlerts().getLeadDepartAlert();
|
||||
|
||||
if (sm.updated("liveMapDataSP")) {
|
||||
roadNameStr = QString::fromStdString(lmd.getRoadName());
|
||||
speedLimitAheadValid = lmd.getSpeedLimitAheadValid();
|
||||
@@ -64,7 +86,7 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
|
||||
static int reverse_delay = 0;
|
||||
bool reverse_allowed = false;
|
||||
if (int(car_state.getGearShifter()) != 4) {
|
||||
if (car_state.getGearShifter() != cereal::CarState::GearShifter::REVERSE) {
|
||||
reverse_delay = 0;
|
||||
reverse_allowed = false;
|
||||
} else {
|
||||
@@ -76,46 +98,47 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
|
||||
reversing = reverse_allowed;
|
||||
|
||||
if (sm.updated("liveParameters")) {
|
||||
roll = sm["liveParameters"].getLiveParameters().getRoll();
|
||||
}
|
||||
|
||||
if (sm.updated("deviceState")) {
|
||||
memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent();
|
||||
}
|
||||
|
||||
if (sm.updated(gps_source)) {
|
||||
gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not.
|
||||
altitude = gpsLocation.getAltitude();
|
||||
bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg();
|
||||
bearingDeg = gpsLocation.getBearingDeg();
|
||||
}
|
||||
|
||||
if (sm.updated("liveTorqueParameters")) {
|
||||
torquedUseParams = ltp.getUseParams();
|
||||
latAccelFactorFiltered = ltp.getLatAccelFactorFiltered();
|
||||
frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered();
|
||||
liveValid = ltp.getLiveValid();
|
||||
}
|
||||
|
||||
latActive = car_control.getLatActive();
|
||||
actuators = car_control.getActuators();
|
||||
longOverride = car_control.getCruiseControl().getOverride();
|
||||
carControlEnabled = car_control.getEnabled();
|
||||
|
||||
steerOverride = car_state.getSteeringPressed();
|
||||
|
||||
devUiInfo = s.scene.dev_ui_info;
|
||||
|
||||
speedUnit = is_metric ? tr("km/h") : tr("mph");
|
||||
lead_d_rel = radar_state.getLeadOne().getDRel();
|
||||
lead_v_rel = radar_state.getLeadOne().getVRel();
|
||||
lead_status = radar_state.getLeadOne().getStatus();
|
||||
steerControlType = car_params.getSteerControlType();
|
||||
actuators = car_control.getActuators();
|
||||
torqueLateral = steerControlType == cereal::CarParams::SteerControlType::TORQUE;
|
||||
angleSteers = car_state.getSteeringAngleDeg();
|
||||
desiredCurvature = cs.getDesiredCurvature();
|
||||
curvature = cs.getCurvature();
|
||||
roll = sm["liveParameters"].getLiveParameters().getRoll();
|
||||
memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent();
|
||||
gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not.
|
||||
altitude = gpsLocation.getAltitude();
|
||||
vEgo = car_state.getVEgo();
|
||||
aEgo = car_state.getAEgo();
|
||||
steeringTorqueEps = car_state.getSteeringTorqueEps();
|
||||
bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg();
|
||||
bearingDeg = gpsLocation.getBearingDeg();
|
||||
torquedUseParams = ltp.getUseParams();
|
||||
latAccelFactorFiltered = ltp.getLatAccelFactorFiltered();
|
||||
frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered();
|
||||
liveValid = ltp.getLiveValid();
|
||||
|
||||
standstillTimer = s.scene.standstill_timer;
|
||||
isStandstill = car_state.getStandstill();
|
||||
if (not s.scene.started) standstillElapsedTime = 0.0;
|
||||
longOverride = car_control.getCruiseControl().getOverride();
|
||||
smartCruiseControlVisionEnabled = lp_sp.getSmartCruiseControl().getVision().getEnabled();
|
||||
smartCruiseControlVisionActive = lp_sp.getSmartCruiseControl().getVision().getActive();
|
||||
smartCruiseControlMapEnabled = lp_sp.getSmartCruiseControl().getMap().getEnabled();
|
||||
smartCruiseControlMapActive = lp_sp.getSmartCruiseControl().getMap().getActive();
|
||||
|
||||
greenLightAlert = lp_sp.getE2eAlerts().getGreenLightAlert();
|
||||
leadDepartAlert = lp_sp.getE2eAlerts().getLeadDepartAlert();
|
||||
if (!s.scene.started) standstillElapsedTime = 0.0;
|
||||
|
||||
// override stock current speed values
|
||||
float v_ego = (v_ego_cluster_seen && !s.scene.trueVEgoUI) ? car_state.getVEgoCluster() : car_state.getVEgo();
|
||||
@@ -126,10 +149,11 @@ void HudRendererSP::updateState(const UIState &s) {
|
||||
rightBlinkerOn = car_state.getRightBlinker();
|
||||
leftBlindspot = car_state.getLeftBlindspot();
|
||||
rightBlindspot = car_state.getRightBlindspot();
|
||||
showTurnSignals = s.scene.turn_signals;
|
||||
|
||||
carControlEnabled = car_control.getEnabled();
|
||||
speedCluster = car_state.getCruiseState().getSpeedCluster() * speedConv;
|
||||
|
||||
allow_e2e_alerts = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE &&
|
||||
sm.rcv_frame("driverStateV2") > s.scene.started_frame && !reversing;
|
||||
}
|
||||
|
||||
void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
@@ -222,7 +246,7 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
drawRoadName(p, surface_rect);
|
||||
|
||||
// Green Light & Lead Depart Alerts
|
||||
if (greenLightAlert or leadDepartAlert) {
|
||||
if (greenLightAlert || leadDepartAlert) {
|
||||
e2eAlertDisplayTimer = 3 * UI_FREQ;
|
||||
// reset onroad sleep timer for e2e alerts
|
||||
uiStateSP()->reset_onroad_sleep_timer();
|
||||
@@ -232,11 +256,11 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
e2eAlertFrame++;
|
||||
if (greenLightAlert) {
|
||||
alert_text = tr("GREEN\nLIGHT");
|
||||
alert_img = devUiInfo > 0 ? green_light_alert_small_img : green_light_alert_large_img;
|
||||
alert_img = green_light_alert_img;
|
||||
}
|
||||
else if (leadDepartAlert) {
|
||||
alert_text = tr("LEAD VEHICLE\nDEPARTING");
|
||||
alert_img = devUiInfo > 0 ? lead_depart_alert_small_img : lead_depart_alert_large_img;
|
||||
alert_img = lead_depart_alert_img;
|
||||
}
|
||||
drawE2eAlert(p, surface_rect);
|
||||
}
|
||||
@@ -254,7 +278,7 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
// No Alerts displayed
|
||||
else {
|
||||
e2eAlertFrame = 0;
|
||||
if (not isStandstill) standstillElapsedTime = 0.0;
|
||||
if (!isStandstill) standstillElapsedTime = 0.0;
|
||||
}
|
||||
|
||||
// Blinker
|
||||
@@ -545,7 +569,8 @@ void HudRendererSP::drawSpeedLimitSigns(QPainter &p, QRect &sign_rect) {
|
||||
}
|
||||
|
||||
void HudRendererSP::drawUpcomingSpeedLimit(QPainter &p) {
|
||||
bool speed_limit_ahead = speedLimitAheadValid && speedLimitAhead > 0 && speedLimitAhead != speedLimit && speedLimitAheadValidFrame > 0;
|
||||
bool speed_limit_ahead = speedLimitAheadValid && speedLimitAhead > 0 && speedLimitAhead != speedLimit && speedLimitAheadValidFrame > 0 &&
|
||||
speedLimitSource == cereal::LongitudinalPlanSP::SpeedLimit::Source::MAP;
|
||||
if (!speed_limit_ahead) {
|
||||
return;
|
||||
}
|
||||
@@ -639,7 +664,7 @@ void HudRendererSP::drawRoadName(QPainter &p, const QRect &surface_rect) {
|
||||
|
||||
void HudRendererSP::drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect) {
|
||||
const int sign_margin = 12;
|
||||
const int arrow_spacing = sign_margin * 3;
|
||||
const int arrow_spacing = sign_margin * 1.4;
|
||||
int arrow_x = sign_rect.right() + arrow_spacing;
|
||||
|
||||
int _set_speed = std::nearbyint(set_speed);
|
||||
@@ -689,7 +714,7 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
}
|
||||
|
||||
// Draw "MAX" or carState.cruiseState.speedCluster (when ICBM is active) text
|
||||
if (carControlEnabled) {
|
||||
if (!pcmCruiseSpeed && carControlEnabled) {
|
||||
if (std::nearbyint(set_speed) != std::nearbyint(speedCluster)) {
|
||||
icbm_active_counter = 3 * UI_FREQ;
|
||||
} else if (icbm_active_counter > 0) {
|
||||
@@ -714,26 +739,26 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
}
|
||||
|
||||
void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text) {
|
||||
int size = devUiInfo > 0 ? e2e_alert_small : e2e_alert_large;
|
||||
int x = surface_rect.center().x() + surface_rect.width() / 4;
|
||||
if (!allow_e2e_alerts) return;
|
||||
|
||||
int x = surface_rect.right() - e2e_alert_size - (devUiInfo > 0 ? 180 : 100) - (UI_BORDER_SIZE * 3);
|
||||
int y = surface_rect.center().y() + 20;
|
||||
x += devUiInfo > 0 ? 0 : 50;
|
||||
QRect alertRect(x - size, y - size, size * 2, size * 2);
|
||||
QRect alertRect(x - e2e_alert_size, y - e2e_alert_size, e2e_alert_size * 2, e2e_alert_size * 2);
|
||||
|
||||
// Alert Circle
|
||||
QPoint center = alertRect.center();
|
||||
QColor frameColor;
|
||||
if (not alert_alt_text.isEmpty()) frameColor = QColor(255, 255, 255, 75);
|
||||
if (!alert_alt_text.isEmpty()) frameColor = QColor(255, 255, 255, 75);
|
||||
else frameColor = pulseElement(e2eAlertFrame) ? QColor(255, 255, 255, 75) : QColor(0, 255, 0, 75);
|
||||
p.setPen(QPen(frameColor, 15));
|
||||
p.setBrush(QColor(0, 0, 0, 190));
|
||||
p.drawEllipse(center, size, size);
|
||||
p.drawEllipse(center, e2e_alert_size, e2e_alert_size);
|
||||
|
||||
// Alert Text
|
||||
QColor txtColor;
|
||||
QFont font;
|
||||
int alert_bottom_adjustment;
|
||||
if (not alert_alt_text.isEmpty()) {
|
||||
if (!alert_alt_text.isEmpty()) {
|
||||
font = InterFont(100, QFont::Bold);
|
||||
alert_bottom_adjustment = 5;
|
||||
txtColor = QColor(255, 255, 255, 255);
|
||||
@@ -750,7 +775,7 @@ void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const Q
|
||||
textRect.moveBottom(alertRect.bottom() - alertRect.height() / alert_bottom_adjustment);
|
||||
p.drawText(textRect, Qt::AlignCenter, alert_text);
|
||||
|
||||
if (not alert_alt_text.isEmpty()) {
|
||||
if (!alert_alt_text.isEmpty()) {
|
||||
// Alert Alternate Text
|
||||
p.setFont(InterFont(80, QFont::Bold));
|
||||
p.setPen(QColor(255, 175, 3, 240));
|
||||
@@ -779,7 +804,7 @@ void HudRendererSP::drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
|
||||
void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
|
||||
const bool hazard = leftBlinkerOn && rightBlinkerOn;
|
||||
int blinkerStatus = hazard ? 2 : (leftBlinkerOn or rightBlinkerOn) ? 1 : 0;
|
||||
int blinkerStatus = hazard ? 2 : (leftBlinkerOn || rightBlinkerOn) ? 1 : 0;
|
||||
|
||||
if (!leftBlinkerOn && !rightBlinkerOn) {
|
||||
blinkerFrameCounter = 0;
|
||||
|
||||
@@ -83,6 +83,7 @@ private:
|
||||
bool speedLimitValid;
|
||||
bool speedLimitLastValid;
|
||||
float speedLimitFinalLast;
|
||||
cereal::LongitudinalPlanSP::SpeedLimit::Source speedLimitSource;
|
||||
bool speedLimitAheadValid;
|
||||
float speedLimitAhead;
|
||||
float speedLimitAheadDistance;
|
||||
@@ -96,16 +97,14 @@ private:
|
||||
int speedLimitAssistFrame;
|
||||
QPixmap plus_arrow_up_img;
|
||||
QPixmap minus_arrow_down_img;
|
||||
int e2e_alert_small = 250;
|
||||
int e2e_alert_large = 300;
|
||||
QPixmap green_light_alert_small_img;
|
||||
QPixmap green_light_alert_large_img;
|
||||
int e2e_alert_size = 250;
|
||||
QPixmap green_light_alert_img;
|
||||
bool greenLightAlert;
|
||||
int e2eAlertFrame;
|
||||
int e2eAlertDisplayTimer = 0;
|
||||
bool allow_e2e_alerts;
|
||||
bool leadDepartAlert;
|
||||
QPixmap lead_depart_alert_small_img;
|
||||
QPixmap lead_depart_alert_large_img;
|
||||
QPixmap lead_depart_alert_img;
|
||||
QString alert_text;
|
||||
QPixmap alert_img;
|
||||
bool hideVEgoUI;
|
||||
@@ -120,4 +119,5 @@ private:
|
||||
bool carControlEnabled;
|
||||
float speedCluster = 0;
|
||||
int icbm_active_counter = 0;
|
||||
bool pcmCruiseSpeed = true;
|
||||
};
|
||||
|
||||
@@ -21,74 +21,73 @@ void ModelRendererSP::update_model(const cereal::ModelDataV2::Reader &model, con
|
||||
mapLineToPolygon(model.getLaneLines()[2], 0.2, -0.05, &right_blindspot_vertices, max_idx_barrier);
|
||||
}
|
||||
|
||||
void ModelRendererSP::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) {
|
||||
void ModelRendererSP::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
auto *s = uiState();
|
||||
auto &sm = *(s->sm);
|
||||
bool blindspot = s->scene.blindspot_ui;
|
||||
|
||||
if (blindspot) {
|
||||
bool left_blindspot = sm["carState"].getCarState().getLeftBlindspot();
|
||||
bool right_blindspot = sm["carState"].getCarState().getRightBlindspot();
|
||||
|
||||
//painter.setBrush(QColor::fromRgbF(1.0, 0.0, 0.0, 0.4)); // Red with alpha for blind spot
|
||||
|
||||
if (left_blindspot && !left_blindspot_vertices.isEmpty()) {
|
||||
QLinearGradient gradient(0, 0, surface_rect.width(), 0); // Horizontal gradient from left to right
|
||||
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
|
||||
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
|
||||
painter.setBrush(gradient);
|
||||
painter.drawPolygon(left_blindspot_vertices);
|
||||
}
|
||||
|
||||
if (right_blindspot && !right_blindspot_vertices.isEmpty()) {
|
||||
QLinearGradient gradient(surface_rect.width(), 0, 0, 0); // Horizontal gradient from right to left
|
||||
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
|
||||
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
|
||||
painter.setBrush(gradient);
|
||||
painter.drawPolygon(right_blindspot_vertices);
|
||||
}
|
||||
if (sm.rcv_frame("liveCalibration") < s->scene.started_frame ||
|
||||
sm.rcv_frame("modelV2") < s->scene.started_frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool rainbow = s->scene.rainbow_mode;
|
||||
//float v_ego = sm["carState"].getCarState().getVEgo();
|
||||
clip_region = surface_rect.adjusted(-CLIP_MARGIN, -CLIP_MARGIN, CLIP_MARGIN, CLIP_MARGIN);
|
||||
experimental_mode = sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
|
||||
longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl();
|
||||
path_offset_z = sm["liveCalibration"].getLiveCalibration().getHeight()[0];
|
||||
|
||||
if (rainbow) {
|
||||
// Simple time-based animation
|
||||
float time_offset = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count() / 1000.0f;
|
||||
painter.save();
|
||||
|
||||
// simple linear gradient from bottom to top
|
||||
QLinearGradient bg(0, surface_rect.height(), 0, 0);
|
||||
const auto &model = sm["modelV2"].getModelV2();
|
||||
const auto &radar_state = sm["radarState"].getRadarState();
|
||||
const auto &lead_one = radar_state.getLeadOne();
|
||||
const auto &car_state = sm["carState"].getCarState();
|
||||
|
||||
// evenly spaced colors across the spectrum
|
||||
// The animation shifts the entire spectrum smoothly
|
||||
float animation_speed = 40.0f; // speed vroom vroom
|
||||
float hue_offset = fmod(time_offset * animation_speed, 360.0f);
|
||||
update_model(model, lead_one);
|
||||
drawLaneLines(painter);
|
||||
|
||||
// 6-8 color stops for smooth transitions more color makes it laggy
|
||||
const int num_stops = 7;
|
||||
for (int i = 0; i < num_stops; i++) {
|
||||
float position = static_cast<float>(i) / (num_stops - 1);
|
||||
|
||||
float hue = fmod(hue_offset + position * 360.0f, 360.0f);
|
||||
float saturation = 0.9f;
|
||||
float lightness = 0.6f;
|
||||
|
||||
// Alpha fades out towards the far end of the path
|
||||
float alpha = 0.8f * (1.0f - position * 0.3f);
|
||||
|
||||
QColor color = QColor::fromHslF(hue / 360.0f, saturation, lightness, alpha);
|
||||
bg.setColorAt(position, color);
|
||||
}
|
||||
|
||||
painter.setBrush(bg);
|
||||
painter.drawPolygon(track_vertices);
|
||||
if (s->scene.rainbow_mode) {
|
||||
drawRainbowPath(painter, surface_rect);
|
||||
} else {
|
||||
// Normal path rendering
|
||||
ModelRenderer::drawPath(painter, model, surface_rect.height());
|
||||
}
|
||||
|
||||
if (longitudinal_control && sm.alive("radarState")) {
|
||||
update_leads(radar_state, model.getPosition());
|
||||
const auto &lead_two = radar_state.getLeadTwo();
|
||||
if (lead_one.getStatus()) {
|
||||
drawLead(painter, lead_one, lead_vertices[0], surface_rect);
|
||||
}
|
||||
if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) {
|
||||
drawLead(painter, lead_two, lead_vertices[1], surface_rect);
|
||||
}
|
||||
}
|
||||
|
||||
if (s->scene.blindspot_ui) {
|
||||
const bool left_blindspot = car_state.getLeftBlindspot();
|
||||
const bool right_blindspot = car_state.getRightBlindspot();
|
||||
drawBlindspot(painter, surface_rect, left_blindspot, right_blindspot);
|
||||
}
|
||||
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
void ModelRendererSP::drawBlindspot(QPainter &painter, const QRect &surface_rect, bool left_blindspot, bool right_blindspot) {
|
||||
if (left_blindspot && !left_blindspot_vertices.isEmpty()) {
|
||||
QLinearGradient gradient(0, 0, surface_rect.width(), 0); // Horizontal gradient from left to right
|
||||
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
|
||||
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
|
||||
painter.setBrush(gradient);
|
||||
painter.drawPolygon(left_blindspot_vertices);
|
||||
}
|
||||
|
||||
if (right_blindspot && !right_blindspot_vertices.isEmpty()) {
|
||||
QLinearGradient gradient(surface_rect.width(), 0, 0, 0); // Horizontal gradient from right to left
|
||||
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
|
||||
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
|
||||
painter.setBrush(gradient);
|
||||
painter.drawPolygon(right_blindspot_vertices);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelRendererSP::drawLeadStatus(QPainter &painter, int height, int width) {
|
||||
@@ -121,19 +120,16 @@ void ModelRendererSP::drawLeadStatus(QPainter &painter, int height, int width) {
|
||||
}
|
||||
|
||||
if (has_lead_one) {
|
||||
drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1");
|
||||
drawLeadStatusPosition(painter, lead_one, lead_vertices[0], height, width);
|
||||
}
|
||||
|
||||
if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) {
|
||||
drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2");
|
||||
drawLeadStatusPosition(painter, lead_two, lead_vertices[1], height, width);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelRendererSP::drawLeadStatusAtPosition(QPainter &painter,
|
||||
const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &chevron_pos,
|
||||
int height, int width,
|
||||
const QString &label) {
|
||||
void ModelRendererSP::drawLeadStatusPosition(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &chevron_pos, int height, int width) {
|
||||
float d_rel = lead_data.getDRel();
|
||||
float v_rel = lead_data.getVRel();
|
||||
auto *s = uiState();
|
||||
@@ -223,3 +219,36 @@ void ModelRendererSP::drawLeadStatusAtPosition(QPainter &painter,
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
}
|
||||
|
||||
void ModelRendererSP::drawRainbowPath(QPainter &painter, const QRect &surface_rect) {
|
||||
// Simple time-based animation
|
||||
float time_offset = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count() / 1000.0f;
|
||||
|
||||
// simple linear gradient from bottom to top
|
||||
QLinearGradient bg(0, surface_rect.height(), 0, 0);
|
||||
|
||||
// evenly spaced colors across the spectrum
|
||||
// The animation shifts the entire spectrum smoothly
|
||||
float animation_speed = 40.0f; // speed vroom vroom
|
||||
float hue_offset = fmod(time_offset * animation_speed, 360.0f);
|
||||
|
||||
// 6-8 color stops for smooth transitions more color makes it laggy
|
||||
const int num_stops = 7;
|
||||
for (int i = 0; i < num_stops; i++) {
|
||||
float position = static_cast<float>(i) / (num_stops - 1);
|
||||
|
||||
float hue = fmod(hue_offset + position * 360.0f, 360.0f);
|
||||
float saturation = 0.9f;
|
||||
float lightness = 0.6f;
|
||||
|
||||
// Alpha fades out towards the far end of the path
|
||||
float alpha = 0.8f * (1.0f - position * 0.3f);
|
||||
|
||||
QColor color = QColor::fromHslF(hue / 360.0f, saturation, lightness, alpha);
|
||||
bg.setColorAt(position, color);
|
||||
}
|
||||
|
||||
painter.setBrush(bg);
|
||||
painter.drawPolygon(track_vertices);
|
||||
}
|
||||
|
||||
@@ -13,17 +13,15 @@ class ModelRendererSP : public ModelRenderer {
|
||||
public:
|
||||
ModelRendererSP() = default;
|
||||
|
||||
void draw(QPainter &painter, const QRect &surface_rect);
|
||||
|
||||
private:
|
||||
void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) override;
|
||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &rect) override;
|
||||
|
||||
// Lead status display methods
|
||||
void drawLeadStatus(QPainter &painter, int height, int width);
|
||||
void drawLeadStatusAtPosition(QPainter &painter,
|
||||
const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &chevron_pos,
|
||||
int height, int width,
|
||||
const QString &label);
|
||||
void drawLeadStatusPosition(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
|
||||
const QPointF &chevron_pos, int height, int width);
|
||||
void drawBlindspot(QPainter &painter, const QRect &surface_rect, bool left_blindspot, bool right_blindspot);
|
||||
void drawRainbowPath(QPainter &painter, const QRect &surface_rect);
|
||||
|
||||
QPolygonF left_blindspot_vertices;
|
||||
QPolygonF right_blindspot_vertices;
|
||||
|
||||
@@ -122,3 +122,7 @@ std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasIntelligentCruiseButtonManagement(const cereal::CarParamsSP::Reader &car_params_sp) {
|
||||
return car_params_sp.getIntelligentCruiseButtonManagementAvailable() && Params().getBool("IntelligentCruiseButtonManagement");
|
||||
}
|
||||
|
||||
@@ -23,3 +23,4 @@ std::optional<QString> getParamIgnoringDefault(const std::string ¶m_name, co
|
||||
QMap<QString, QVariantMap> loadPlatformList();
|
||||
QStringList searchFromList(const QString &query, const QStringList &list);
|
||||
std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::string& _param);
|
||||
bool hasIntelligentCruiseButtonManagement(const cereal::CarParamsSP::Reader &car_params_sp);
|
||||
|
||||
@@ -390,7 +390,7 @@ class ButtonParamControlSP : public MultiButtonControlSP {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon,
|
||||
const std::vector<QString> &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon,
|
||||
const std::vector<QString> &button_texts, const int minimum_button_width = 380, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon,
|
||||
button_texts, minimum_button_width, inline_layout, advancedControl) {
|
||||
key = param.toStdString();
|
||||
int value = atoi(params.get(key).c_str());
|
||||
|
||||
@@ -29,7 +29,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) {
|
||||
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan",
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters",
|
||||
"carStateSP", "liveParameters", "liveMapDataSP"
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP"
|
||||
});
|
||||
|
||||
// update timer
|
||||
|
||||
@@ -18,7 +18,7 @@ if __name__ == "__main__":
|
||||
while True:
|
||||
print("setting alert update")
|
||||
params.put_bool("UpdateAvailable", True)
|
||||
r = open(os.path.join(BASEDIR, "RELEASES.md")).read()
|
||||
r = open(os.path.join(BASEDIR, "CHANGELOG.md")).read()
|
||||
r = r[:r.find('\n\n')] # Slice latest release notes
|
||||
params.put("UpdaterNewReleaseNotes", r + "\n")
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ def setup_offroad_alert(click, pm: PubMaster, scroll=None):
|
||||
|
||||
def setup_update_available(click, pm: PubMaster, scroll=None):
|
||||
Params().put_bool("UpdateAvailable", True)
|
||||
release_notes_path = os.path.join(BASEDIR, "RELEASES.md")
|
||||
release_notes_path = os.path.join(BASEDIR, "CHANGELOG.md")
|
||||
with open(release_notes_path) as file:
|
||||
release_notes = file.read().split('\n\n', 1)[0]
|
||||
Params().put("UpdaterNewReleaseNotes", release_notes + "\n")
|
||||
|
||||
@@ -1778,62 +1778,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>sunnypilot</source>
|
||||
<translation>sunnypilot</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>None</source>
|
||||
<translation>없음</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fixed</source>
|
||||
<translation>고정</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Percent</source>
|
||||
<translation>비율</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Car
|
||||
Only</source>
|
||||
<translation>차량만</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Map
|
||||
Only</source>
|
||||
<translation>지도만</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Car
|
||||
First</source>
|
||||
<translation>차량
|
||||
우선</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Map
|
||||
First</source>
|
||||
<translation>지도
|
||||
우선</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Combined
|
||||
Data</source>
|
||||
<translation>결합
|
||||
데이터</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Off</source>
|
||||
<translation>끄기</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Information</source>
|
||||
<translation>정보</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Warning</source>
|
||||
<translation>경고</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Assist</source>
|
||||
<translation>보조</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsWindow</name>
|
||||
@@ -2198,6 +2142,34 @@ Data</source>
|
||||
<source>⦿ Combined: Use combined Speed Limit data from Car & OpenStreetMaps</source>
|
||||
<translation>⦿ 결합: 차량 및 OpenStreetMaps의 속도 제한 결합 데이터 사용</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Car
|
||||
Only</source>
|
||||
<translation>차량만</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Map
|
||||
Only</source>
|
||||
<translation>지도만</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Car
|
||||
First</source>
|
||||
<translation>차량
|
||||
우선</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Map
|
||||
First</source>
|
||||
<translation>지도
|
||||
우선</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Combined
|
||||
Data</source>
|
||||
<translation>결합
|
||||
데이터</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SpeedLimitSettings</name>
|
||||
@@ -2245,6 +2217,34 @@ Data</source>
|
||||
<source>⦿ Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons.</source>
|
||||
<translation>⦿ 보조: +/- 버튼을 조작할 때 현재 도로의 제한 속도를 기준으로 차량의 크루즈 속도를 조정합니다.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>None</source>
|
||||
<translation>없음</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fixed</source>
|
||||
<translation>고정</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Percent</source>
|
||||
<translation>비율</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Off</source>
|
||||
<translation>끄기</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Information</source>
|
||||
<translation>정보</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Warning</source>
|
||||
<translation>경고</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Assist</source>
|
||||
<translation>보조</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SshControl</name>
|
||||
|
||||
@@ -5,6 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from enum import IntEnum
|
||||
import hashlib
|
||||
|
||||
PARAMS_UPDATE_PERIOD = 3 # seconds
|
||||
@@ -16,3 +17,23 @@ def get_file_hash(path: str) -> str:
|
||||
for byte_block in iter(lambda: f.read(4096), b""):
|
||||
sha256_hash.update(byte_block)
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
|
||||
class IntEnumBase(IntEnum):
|
||||
@classmethod
|
||||
def min(cls):
|
||||
return min(cls)
|
||||
|
||||
@classmethod
|
||||
def max(cls):
|
||||
return max(cls)
|
||||
|
||||
|
||||
def get_sanitize_int_param(key: str, min_val: int, max_val: int, params) -> int:
|
||||
val: int = params.get(key, return_default=True)
|
||||
clipped_val = max(min_val, min(max_val, val))
|
||||
|
||||
if clipped_val != val:
|
||||
params.put(key, clipped_val)
|
||||
|
||||
return clipped_val
|
||||
|
||||
1
sunnypilot/common/version.h
Normal file
1
sunnypilot/common/version.h
Normal file
@@ -0,0 +1 @@
|
||||
#define SUNNYPILOT_VERSION "2025.003.000"
|
||||
@@ -8,8 +8,12 @@ from abc import abstractmethod, ABC
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||
from openpilot.sunnypilot.navd.helpers import coordinate_from_param
|
||||
|
||||
MAX_SPEED_LIMIT = V_CRUISE_UNSET * CV.KPH_TO_MS
|
||||
|
||||
|
||||
class BaseMapData(ABC):
|
||||
def __init__(self):
|
||||
@@ -46,9 +50,9 @@ class BaseMapData(ABC):
|
||||
mapd_sp_send.valid = self.sm['liveLocationKalman'].gpsOK
|
||||
live_map_data = mapd_sp_send.liveMapDataSP
|
||||
|
||||
live_map_data.speedLimitValid = bool(speed_limit > 0)
|
||||
live_map_data.speedLimitValid = bool(MAX_SPEED_LIMIT > speed_limit > 0)
|
||||
live_map_data.speedLimit = speed_limit
|
||||
live_map_data.speedLimitAheadValid = bool(next_speed_limit > 0)
|
||||
live_map_data.speedLimitAheadValid = bool(MAX_SPEED_LIMIT > next_speed_limit > 0)
|
||||
live_map_data.speedLimitAhead = next_speed_limit
|
||||
live_map_data.speedLimitAheadDistance = next_speed_limit_distance
|
||||
live_map_data.roadName = self.get_current_road_name()
|
||||
|
||||
@@ -116,7 +116,7 @@ class ModelCache:
|
||||
|
||||
class ModelFetcher:
|
||||
"""Handles fetching and caching of model data from remote source"""
|
||||
MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v7.json"
|
||||
MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v8.json"
|
||||
|
||||
def __init__(self, params: Params):
|
||||
self.params = params
|
||||
|
||||
@@ -37,7 +37,7 @@ class CarSpecificEventsSP:
|
||||
# TODO-SP: add 1 m/s hysteresis
|
||||
if CS.vEgo >= self.CP.minEnableSpeed:
|
||||
self.low_speed_alert = False
|
||||
if CS.gearShifter != GearShifter.drive:
|
||||
if self.CP.minEnableSpeed >= 14.5 and CS.gearShifter != GearShifter.drive:
|
||||
self.low_speed_alert = True
|
||||
if self.low_speed_alert:
|
||||
events.add(EventName.belowSteerSpeed)
|
||||
|
||||
@@ -11,9 +11,12 @@ from opendbc.car.interfaces import CarInterfaceBase
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import get_nn_model_path
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import set_speed_limit_assist_availability
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from sunnypilot.sunnylink.statsd import STATSLOGSP
|
||||
|
||||
|
||||
def log_fingerprint(CP: structs.CarParams) -> None:
|
||||
if CP.carFingerprint == "MOCK":
|
||||
@@ -66,6 +69,29 @@ def _initialize_torque_lateral_control(CI: CarInterfaceBase, CP: structs.CarPara
|
||||
CI.configure_torque_tune(CP.carFingerprint, CP.lateralTuning)
|
||||
|
||||
|
||||
def _cleanup_unsupported_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None) -> None:
|
||||
if params is None:
|
||||
params = Params()
|
||||
|
||||
if CP.steerControlType == structs.CarParams.SteerControlType.angle:
|
||||
cloudlog.warning("SteerControlType is angle, cleaning up params")
|
||||
params.remove("NeuralNetworkLateralControl")
|
||||
params.remove("EnforceTorqueControl")
|
||||
|
||||
if not CP_SP.intelligentCruiseButtonManagementAvailable or CP.openpilotLongitudinalControl:
|
||||
cloudlog.warning("ICBM not available or openpilot Longitudinal Control enabled, cleaning up params")
|
||||
params.remove("IntelligentCruiseButtonManagement")
|
||||
|
||||
if not CP.openpilotLongitudinalControl and CP_SP.pcmCruiseSpeed:
|
||||
cloudlog.warning("openpilot Longitudinal Control and ICBM not available, cleaning up params")
|
||||
params.remove("DynamicExperimentalControl")
|
||||
params.remove("CustomAccIncrementsEnabled")
|
||||
params.remove("SmartCruiseControlVision")
|
||||
params.remove("SmartCruiseControlMap")
|
||||
|
||||
set_speed_limit_assist_availability(CP, CP_SP, params)
|
||||
|
||||
|
||||
def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None:
|
||||
CP = CI.CP
|
||||
CP_SP = CI.CP_SP
|
||||
@@ -74,6 +100,10 @@ def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None:
|
||||
nnlc_enabled = _initialize_neural_network_lateral_control(CP, CP_SP, params)
|
||||
_initialize_intelligent_cruise_button_management(CP, CP_SP, params)
|
||||
_initialize_torque_lateral_control(CI, CP, enforce_torque, nnlc_enabled)
|
||||
_cleanup_unsupported_params(CP, CP_SP)
|
||||
|
||||
STATSLOGSP.raw('sunnypilot.car_params', CP.to_dict())
|
||||
# STATSLOGSP.raw('sunnypilot_params.car_params_sp', CP_SP.to_dict()) # https://github.com/sunnypilot/opendbc/pull/361
|
||||
|
||||
|
||||
def initialize_params(params) -> list[dict[str, Any]]:
|
||||
@@ -84,4 +114,15 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
"HyundaiLongitudinalTuning"
|
||||
])
|
||||
|
||||
# subaru
|
||||
keys.extend([
|
||||
"SubaruStopAndGo",
|
||||
"SubaruStopAndGoManualParkingBrake",
|
||||
])
|
||||
|
||||
# tesla
|
||||
keys.extend([
|
||||
"TeslaCoopSteering",
|
||||
])
|
||||
|
||||
return [{k: params.get(k, return_default=True)} for k in keys]
|
||||
|
||||
@@ -12,47 +12,159 @@ from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
TRIGGER_THRESHOLD = 30
|
||||
GREEN_LIGHT_X_THRESHOLD = 30
|
||||
LEAD_DEPART_DIST_THRESHOLD = 1.0
|
||||
TRIGGER_TIMER_THRESHOLD = 0.3
|
||||
|
||||
|
||||
class E2EStates:
|
||||
INACTIVE = 0
|
||||
ARMED = 1
|
||||
CONSUMED = 2
|
||||
|
||||
|
||||
class E2EAlertsHelper:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._frame = -1
|
||||
self.frame = -1
|
||||
self.green_light_state = E2EStates.INACTIVE
|
||||
self.prev_green_light_state = E2EStates.INACTIVE
|
||||
self.lead_depart_state = E2EStates.INACTIVE
|
||||
self.prev_lead_depart_state = E2EStates.INACTIVE
|
||||
|
||||
self.green_light_alert = False
|
||||
self.green_light_alert_enabled = self._params.get_bool("GreenLightAlert")
|
||||
self.lead_depart_alert = False
|
||||
self.lead_depart_alert_enabled = self._params.get_bool("LeadDepartAlert")
|
||||
|
||||
self.green_light_trigger_timer = 0
|
||||
self.lead_depart_trigger_timer = 0
|
||||
self.last_lead_distance = -1
|
||||
self.last_moving_frame = -1
|
||||
|
||||
self.allowed = False
|
||||
self.last_allowed = False
|
||||
self.has_lead = False
|
||||
|
||||
self.lead_depart_arm_timer = 0
|
||||
self.lead_depart_confirmed_lead = False
|
||||
self.lead_depart_armed = False
|
||||
|
||||
def _read_params(self) -> None:
|
||||
if self._frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
self.green_light_alert_enabled = self._params.get_bool("GreenLightAlert")
|
||||
self.lead_depart_alert_enabled = self._params.get_bool("LeadDepartAlert")
|
||||
|
||||
self._frame += 1
|
||||
|
||||
def update(self, sm: messaging.SubMaster, events_sp: EventsSP) -> None:
|
||||
self._read_params()
|
||||
|
||||
if not (self.green_light_alert_enabled or self.lead_depart_alert_enabled):
|
||||
return
|
||||
|
||||
def update_alert_trigger(self, sm: messaging.SubMaster):
|
||||
CS = sm['carState']
|
||||
CC = sm['carControl']
|
||||
|
||||
model_x = sm['modelV2'].position.x
|
||||
max_idx = len(model_x) - 1
|
||||
has_lead = sm['radarState'].leadOne.status
|
||||
lead_vRel: float = sm['radarState'].leadOne.vRel
|
||||
self.has_lead = sm['radarState'].leadOne.status
|
||||
lead_dRel = sm['radarState'].leadOne.dRel
|
||||
|
||||
# Green light alert
|
||||
self.green_light_alert = (self.green_light_alert_enabled and model_x[max_idx] > TRIGGER_THRESHOLD
|
||||
and not has_lead and CS.standstill and not CS.gasPressed and not CC.enabled)
|
||||
standstill = CS.standstill
|
||||
moving = not standstill and CS.vEgo > 0.1
|
||||
|
||||
if moving:
|
||||
self.last_moving_frame = self.frame
|
||||
recent_moving = self.last_moving_frame == -1 or (self.frame - self.last_moving_frame) * DT_MDL < 2.0
|
||||
|
||||
self.allowed = not moving and not CS.gasPressed and not CC.enabled and not recent_moving
|
||||
|
||||
# Green Light Alert
|
||||
green_light_trigger = False
|
||||
if self.green_light_state == E2EStates.ARMED:
|
||||
if model_x[max_idx] > GREEN_LIGHT_X_THRESHOLD:
|
||||
self.green_light_trigger_timer += 1
|
||||
else:
|
||||
self.green_light_trigger_timer = 0
|
||||
|
||||
if self.green_light_trigger_timer * DT_MDL > TRIGGER_TIMER_THRESHOLD:
|
||||
green_light_trigger = True
|
||||
elif self.green_light_state != E2EStates.ARMED:
|
||||
self.green_light_trigger_timer = 0
|
||||
|
||||
# Lead Departure Alert
|
||||
self.lead_depart_alert = (self.lead_depart_alert_enabled and CS.standstill and model_x[max_idx] > 30
|
||||
and has_lead and lead_vRel > 1 and not CS.gasPressed)
|
||||
close_lead_valid = self.has_lead and lead_dRel < 8.0
|
||||
if self.allowed and not self.last_allowed and close_lead_valid:
|
||||
self.lead_depart_confirmed_lead = True
|
||||
elif not self.allowed:
|
||||
self.lead_depart_confirmed_lead = False
|
||||
|
||||
if self.allowed and self.lead_depart_confirmed_lead and close_lead_valid:
|
||||
self.lead_depart_arm_timer += 1
|
||||
|
||||
if self.lead_depart_arm_timer * DT_MDL >= 1.0:
|
||||
self.lead_depart_armed = True
|
||||
else:
|
||||
self.lead_depart_arm_timer = 0
|
||||
self.lead_depart_armed = False
|
||||
|
||||
lead_depart_trigger = False
|
||||
if self.lead_depart_state == E2EStates.ARMED:
|
||||
if self.last_lead_distance == -1 or lead_dRel < self.last_lead_distance:
|
||||
self.last_lead_distance = lead_dRel
|
||||
|
||||
if self.last_lead_distance != -1 and (lead_dRel - self.last_lead_distance > LEAD_DEPART_DIST_THRESHOLD):
|
||||
self.lead_depart_trigger_timer += 1
|
||||
else:
|
||||
self.lead_depart_trigger_timer = 0
|
||||
|
||||
if self.lead_depart_trigger_timer * DT_MDL > TRIGGER_TIMER_THRESHOLD:
|
||||
lead_depart_trigger = True
|
||||
elif self.lead_depart_state != E2EStates.ARMED:
|
||||
self.last_lead_distance = -1
|
||||
self.lead_depart_trigger_timer = 0
|
||||
|
||||
self.last_allowed = self.allowed
|
||||
|
||||
return green_light_trigger, lead_depart_trigger
|
||||
|
||||
@staticmethod
|
||||
def update_state_machine(state: int, enabled: bool, allowed: bool, triggered: bool) -> tuple[int, bool]:
|
||||
if state != E2EStates.INACTIVE:
|
||||
if not allowed or not enabled:
|
||||
state = E2EStates.INACTIVE
|
||||
|
||||
else:
|
||||
if state == E2EStates.ARMED:
|
||||
if triggered:
|
||||
state = E2EStates.CONSUMED
|
||||
|
||||
elif state == E2EStates.CONSUMED:
|
||||
pass
|
||||
|
||||
elif state == E2EStates.INACTIVE:
|
||||
if allowed and enabled:
|
||||
state = E2EStates.ARMED
|
||||
|
||||
return state, triggered
|
||||
|
||||
def update(self, sm: messaging.SubMaster, events_sp: EventsSP) -> None:
|
||||
self._read_params()
|
||||
|
||||
green_light_trigger, lead_depart_trigger = self.update_alert_trigger(sm)
|
||||
|
||||
self.prev_green_light_state = self.green_light_state
|
||||
self.prev_lead_depart_state = self.lead_depart_state
|
||||
|
||||
self.green_light_state, self.green_light_alert = self.update_state_machine(
|
||||
self.green_light_state,
|
||||
self.green_light_alert_enabled,
|
||||
self.allowed and not self.has_lead,
|
||||
green_light_trigger
|
||||
)
|
||||
|
||||
self.lead_depart_state, self.lead_depart_alert = self.update_state_machine(
|
||||
self.lead_depart_state,
|
||||
self.lead_depart_alert_enabled,
|
||||
self.allowed and self.lead_depart_armed,
|
||||
lead_depart_trigger
|
||||
)
|
||||
|
||||
if self.green_light_alert or self.lead_depart_alert:
|
||||
events_sp.add(custom.OnroadEventSP.EventName.e2eChime)
|
||||
|
||||
self.frame += 1
|
||||
|
||||
@@ -9,13 +9,15 @@ from cereal import custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
|
||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||
|
||||
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
|
||||
|
||||
|
||||
class LaneTurnController:
|
||||
def __init__(self, desire_helper):
|
||||
self.DH = desire_helper
|
||||
self.turn_direction = custom.TurnDirection.none
|
||||
self.turn_direction = TurnDirection.none
|
||||
self.params = Params()
|
||||
self.lane_turn_value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
|
||||
self.param_read_counter = 0
|
||||
@@ -33,13 +35,13 @@ class LaneTurnController:
|
||||
|
||||
def update_lane_turn(self, blindspot_left: bool, blindspot_right: bool, left_blinker: bool, right_blinker: bool, v_ego: float) -> None:
|
||||
if left_blinker and not right_blinker and v_ego < self.lane_turn_value and not blindspot_left:
|
||||
self.turn_direction = custom.TurnDirection.turnLeft
|
||||
self.turn_direction = TurnDirection.turnLeft
|
||||
elif right_blinker and not left_blinker and v_ego < self.lane_turn_value and not blindspot_right:
|
||||
self.turn_direction = custom.TurnDirection.turnRight
|
||||
self.turn_direction = TurnDirection.turnRight
|
||||
else:
|
||||
self.turn_direction = custom.TurnDirection.none
|
||||
self.turn_direction = TurnDirection.none
|
||||
|
||||
def get_turn_direction(self):
|
||||
if not self.enabled:
|
||||
return custom.TurnDirection.none
|
||||
return TurnDirection.none
|
||||
return self.turn_direction
|
||||
|
||||
@@ -22,13 +22,13 @@ LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||
|
||||
|
||||
class LongitudinalPlannerSP:
|
||||
def __init__(self, CP: structs.CarParams, mpc):
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
|
||||
self.events_sp = EventsSP()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.dec = DynamicExperimentalController(CP, mpc)
|
||||
self.scc = SmartCruiseControl()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.sla = SpeedLimitAssist(CP)
|
||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||
self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None
|
||||
self.source = LongitudinalPlanSource.cruise
|
||||
self.e2e_alerts_helper = E2EAlertsHelper()
|
||||
|
||||
@@ -4,10 +4,11 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.sunnypilot import IntEnumBase
|
||||
|
||||
|
||||
class Policy(IntEnum):
|
||||
class Policy(IntEnumBase):
|
||||
car_state_only = 0
|
||||
map_data_only = 1
|
||||
car_state_priority = 2
|
||||
@@ -15,13 +16,13 @@ class Policy(IntEnum):
|
||||
combined = 4
|
||||
|
||||
|
||||
class OffsetType(IntEnum):
|
||||
class OffsetType(IntEnumBase):
|
||||
off = 0
|
||||
fixed = 1
|
||||
percentage = 2
|
||||
|
||||
|
||||
class Mode(IntEnum):
|
||||
class Mode(IntEnumBase):
|
||||
off = 0
|
||||
information = 1
|
||||
warning = 2
|
||||
|
||||
@@ -5,7 +5,10 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from cereal import custom, car
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode
|
||||
|
||||
|
||||
def compare_cluster_target(v_cruise_cluster: float, target_set_speed: float, is_metric: bool) -> tuple[bool, bool]:
|
||||
@@ -17,3 +20,25 @@ def compare_cluster_target(v_cruise_cluster: float, target_set_speed: float, is_
|
||||
req_minus = v_cruise_cluster_conv > target_set_speed_conv
|
||||
|
||||
return req_plus, req_minus
|
||||
|
||||
|
||||
def set_speed_limit_assist_availability(CP: car.CarParams, CP_SP: custom.CarParamsSP, params: Params = None) -> bool:
|
||||
if params is None:
|
||||
params = Params()
|
||||
|
||||
is_release = params.get_bool("IsReleaseSpBranch")
|
||||
disallow_in_release = CP.brand == "tesla" and is_release
|
||||
always_disallow = CP.brand == "rivian"
|
||||
allowed = True
|
||||
|
||||
if disallow_in_release or always_disallow:
|
||||
allowed = False
|
||||
|
||||
if not CP.openpilotLongitudinalControl and CP_SP.pcmCruiseSpeed:
|
||||
allowed = False
|
||||
|
||||
if not allowed:
|
||||
if params.get("SpeedLimitMode", return_default=True) == SpeedLimitMode.assist:
|
||||
params.put("SpeedLimitMode", int(SpeedLimitMode.warning))
|
||||
|
||||
return allowed
|
||||
|
||||
@@ -10,13 +10,13 @@ from cereal import custom, car
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target, set_speed_limit_assist_availability
|
||||
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
EventNameSP = custom.OnroadEventSP.EventName
|
||||
@@ -27,14 +27,18 @@ ACTIVE_STATES = (SpeedLimitAssistState.active, SpeedLimitAssistState.adapting)
|
||||
ENABLED_STATES = (SpeedLimitAssistState.preActive, SpeedLimitAssistState.pending, *ACTIVE_STATES)
|
||||
|
||||
DISABLED_GUARD_PERIOD = 0.5 # secs.
|
||||
PRE_ACTIVE_GUARD_PERIOD = 15 # secs. Time to wait after activation before considering temp deactivation signal.
|
||||
# secs. Time to wait after activation before considering temp deactivation signal.
|
||||
PRE_ACTIVE_GUARD_PERIOD = {
|
||||
True: 15,
|
||||
False: 5,
|
||||
}
|
||||
SPEED_LIMIT_CHANGED_HOLD_PERIOD = 1 # secs. Time to wait after speed limit change before switching to preActive.
|
||||
|
||||
LIMIT_MIN_ACC = -1.5 # m/s^2 Maximum deceleration allowed for limit controllers to provide.
|
||||
LIMIT_MAX_ACC = 1.0 # m/s^2 Maximum acceleration allowed for limit controllers to provide while active.
|
||||
LIMIT_MIN_SPEED = 8.33 # m/s, Minimum speed limit to provide as solution on limit controllers.
|
||||
LIMIT_SPEED_OFFSET_TH = -1. # m/s Maximum offset between speed limit and current speed for adapting state.
|
||||
V_CRUISE_UNSET = 255
|
||||
V_CRUISE_UNSET = 255.
|
||||
|
||||
CRUISE_BUTTONS_PLUS = (ButtonType.accelCruise, ButtonType.resumeCruise)
|
||||
CRUISE_BUTTONS_MINUS = (ButtonType.decelCruise, ButtonType.setCruise)
|
||||
@@ -48,13 +52,15 @@ class SpeedLimitAssist:
|
||||
a_ego: float
|
||||
v_offset: float
|
||||
|
||||
def __init__(self, CP):
|
||||
def __init__(self, CP: car.CarParams, CP_SP: custom.CarParamsSP):
|
||||
self.params = Params()
|
||||
self.CP = CP
|
||||
self.CP_SP = CP_SP
|
||||
self.frame = -1
|
||||
self.long_engaged_timer = 0
|
||||
self.pre_active_timer = 0
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
|
||||
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
||||
self.long_enabled = False
|
||||
self.long_enabled_prev = False
|
||||
@@ -109,6 +115,16 @@ class SpeedLimitAssist:
|
||||
def target_set_speed_confirmed(self) -> bool:
|
||||
return bool(self.v_cruise_cluster_conv == self.target_set_speed_conv)
|
||||
|
||||
@property
|
||||
def v_cruise_cluster_below_confirm_speed_threshold(self) -> bool:
|
||||
return bool(self.v_cruise_cluster_conv < CONFIRM_SPEED_THRESHOLD[self.is_metric])
|
||||
|
||||
def update_active_event(self, events_sp: EventsSP) -> None:
|
||||
if self.v_cruise_cluster_below_confirm_speed_threshold:
|
||||
events_sp.add(EventNameSP.speedLimitChanged)
|
||||
else:
|
||||
events_sp.add(EventNameSP.speedLimitActive)
|
||||
|
||||
def get_v_target_from_control(self) -> float:
|
||||
if self._has_speed_limit:
|
||||
if self.pcm_op_long and self.is_enabled:
|
||||
@@ -126,6 +142,7 @@ class SpeedLimitAssist:
|
||||
def update_params(self) -> None:
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
|
||||
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
||||
|
||||
def update_car_state(self, CS: car.CarState) -> None:
|
||||
@@ -175,7 +192,7 @@ class SpeedLimitAssist:
|
||||
@property
|
||||
def apply_confirm_speed_threshold(self) -> bool:
|
||||
# below CST: always require user confirmation
|
||||
if self.v_cruise_cluster_conv < CONFIRM_SPEED_THRESHOLD[self.is_metric]:
|
||||
if self.v_cruise_cluster_below_confirm_speed_threshold:
|
||||
return True
|
||||
|
||||
# at/above CST:
|
||||
@@ -231,7 +248,7 @@ class SpeedLimitAssist:
|
||||
self.state = SpeedLimitAssistState.inactive
|
||||
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
elif self._has_speed_limit and self.v_offset < LIMIT_SPEED_OFFSET_TH:
|
||||
self.state = SpeedLimitAssistState.adapting
|
||||
|
||||
@@ -241,7 +258,7 @@ class SpeedLimitAssist:
|
||||
self.state = SpeedLimitAssistState.inactive
|
||||
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
elif self.v_offset >= LIMIT_SPEED_OFFSET_TH:
|
||||
self.state = SpeedLimitAssistState.active
|
||||
|
||||
@@ -251,7 +268,7 @@ class SpeedLimitAssist:
|
||||
self._update_confirmed_state()
|
||||
elif self.speed_limit_changed:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
|
||||
# PRE_ACTIVE
|
||||
elif self.state == SpeedLimitAssistState.preActive:
|
||||
@@ -277,7 +294,7 @@ class SpeedLimitAssist:
|
||||
self._update_confirmed_state()
|
||||
elif self._has_speed_limit:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
else:
|
||||
self.state = SpeedLimitAssistState.pending
|
||||
|
||||
@@ -303,7 +320,7 @@ class SpeedLimitAssist:
|
||||
|
||||
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
|
||||
# PRE_ACTIVE
|
||||
elif self.state == SpeedLimitAssistState.preActive:
|
||||
@@ -317,7 +334,7 @@ class SpeedLimitAssist:
|
||||
elif self.state == SpeedLimitAssistState.inactive:
|
||||
if self.speed_limit_changed:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
elif self._update_non_pcm_long_confirmed_state():
|
||||
self.state = SpeedLimitAssistState.active
|
||||
|
||||
@@ -333,7 +350,7 @@ class SpeedLimitAssist:
|
||||
self.state = SpeedLimitAssistState.active
|
||||
elif self._has_speed_limit:
|
||||
self.state = SpeedLimitAssistState.preActive
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
|
||||
else:
|
||||
self.state = SpeedLimitAssistState.inactive
|
||||
|
||||
@@ -351,15 +368,15 @@ class SpeedLimitAssist:
|
||||
|
||||
if self.is_active:
|
||||
if self._state_prev not in ACTIVE_STATES:
|
||||
events_sp.add(EventNameSP.speedLimitActive)
|
||||
self.update_active_event(events_sp)
|
||||
|
||||
# only notify if we acquire a valid speed limit
|
||||
# do not check has_speed_limit here
|
||||
elif self._speed_limit != self.speed_limit_prev:
|
||||
if self.speed_limit_prev <= 0:
|
||||
events_sp.add(EventNameSP.speedLimitActive)
|
||||
self.update_active_event(events_sp)
|
||||
elif self.speed_limit_prev > 0 and self._speed_limit > 0:
|
||||
events_sp.add(EventNameSP.speedLimitChanged)
|
||||
self.update_active_event(events_sp)
|
||||
|
||||
def update(self, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float, v_cruise_cluster: float, speed_limit: float,
|
||||
speed_limit_final_last: float, has_speed_limit: bool, distance: float, events_sp: EventsSP) -> None:
|
||||
|
||||
@@ -12,7 +12,7 @@ from openpilot.common.constants import CV
|
||||
from openpilot.common.gps import get_gps_location_service
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD, get_sanitize_int_param
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy, OffsetType
|
||||
|
||||
@@ -42,6 +42,12 @@ class SpeedLimitResolver:
|
||||
self.distance_solutions = {} # Store for distance to current speed limit start for different sources
|
||||
|
||||
self.policy = self.params.get("SpeedLimitPolicy", return_default=True)
|
||||
self.policy = get_sanitize_int_param(
|
||||
"SpeedLimitPolicy",
|
||||
Policy.min().value,
|
||||
Policy.max().value,
|
||||
self.params
|
||||
)
|
||||
self._policy_to_sources_map = {
|
||||
Policy.car_state_only: [SpeedLimitSource.car],
|
||||
Policy.map_data_only: [SpeedLimitSource.map],
|
||||
@@ -54,7 +60,12 @@ class SpeedLimitResolver:
|
||||
self._reset_limit_sources(source)
|
||||
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.offset_type = self.params.get("SpeedLimitOffsetType", return_default=True)
|
||||
self.offset_type = get_sanitize_int_param(
|
||||
"SpeedLimitOffsetType",
|
||||
OffsetType.min().value,
|
||||
OffsetType.max().value,
|
||||
self.params
|
||||
)
|
||||
self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True)
|
||||
|
||||
self.speed_limit = 0.
|
||||
@@ -131,6 +142,7 @@ class SpeedLimitResolver:
|
||||
self.limit_solutions[SpeedLimitSource.map] = speed_limit
|
||||
self.distance_solutions[SpeedLimitSource.map] = 0.
|
||||
|
||||
# FIXME-SP: this is not working as expected
|
||||
if 0. < next_speed_limit < self.v_ego:
|
||||
adapt_time = (next_speed_limit - self.v_ego) / LIMIT_ADAPT_ACC
|
||||
adapt_distance = self.v_ego * adapt_time + 0.5 * LIMIT_ADAPT_ACC * adapt_time ** 2
|
||||
|
||||
@@ -4,17 +4,22 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from cereal import custom
|
||||
|
||||
import pytest
|
||||
|
||||
from cereal import custom
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.rivian.values import CAR as RIVIAN
|
||||
from opendbc.car.tesla.values import CAR as TESLA
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
|
||||
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
@@ -30,16 +35,30 @@ SPEED_LIMITS = {
|
||||
'freeway': 80 * CV.MPH_TO_MS, # 80 mph
|
||||
}
|
||||
|
||||
DEFAULT_CAR = TOYOTA.TOYOTA_RAV4_TSS2
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def car_name(request):
|
||||
return getattr(request, "param", DEFAULT_CAR)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_car_name_on_instance(request, car_name):
|
||||
instance = getattr(request, "instance", None)
|
||||
if instance:
|
||||
instance.car_name = car_name
|
||||
|
||||
|
||||
class TestSpeedLimitAssist:
|
||||
|
||||
def setup_method(self):
|
||||
def setup_method(self, method):
|
||||
self.params = Params()
|
||||
self.reset_custom_params()
|
||||
self.events_sp = EventsSP()
|
||||
CI = self._setup_platform(TOYOTA.TOYOTA_RAV4_TSS2)
|
||||
self.sla = SpeedLimitAssist(CI.CP)
|
||||
self.sla.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
|
||||
CI = self._setup_platform(self.car_name)
|
||||
self.sla = SpeedLimitAssist(CI.CP, CI.CP_SP)
|
||||
self.sla.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.sla.pcm_op_long] / DT_MDL)
|
||||
self.pcm_long_max_set_speed = PCM_LONG_REQUIRED_MAX_SET_SPEED[self.sla.is_metric][1] # use 80 MPH for now
|
||||
self.speed_conv = CV.MS_TO_KPH if self.sla.is_metric else CV.MS_TO_MPH
|
||||
|
||||
@@ -51,10 +70,12 @@ class TestSpeedLimitAssist:
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
CI.CP.openpilotLongitudinalControl = True # always assume it's openpilot longitudinal
|
||||
sunnypilot_interfaces.setup_interfaces(CI, self.params)
|
||||
return CI
|
||||
|
||||
def reset_custom_params(self):
|
||||
self.params.put("IsReleaseSpBranch", True)
|
||||
self.params.put("SpeedLimitMode", int(Mode.assist))
|
||||
self.params.put_bool("IsMetric", False)
|
||||
self.params.put("SpeedLimitOffsetType", 0)
|
||||
@@ -84,6 +105,22 @@ class TestSpeedLimitAssist:
|
||||
assert not self.sla.is_active
|
||||
assert V_CRUISE_UNSET == self.sla.get_v_target_from_control()
|
||||
|
||||
@pytest.mark.parametrize("car_name", [RIVIAN.RIVIAN_R1_GEN1, TESLA.TESLA_MODEL_Y], indirect=True)
|
||||
def test_disallowed_brands(self, car_name):
|
||||
"""
|
||||
Speed Limit Assist is disabled for the following brands and conditions:
|
||||
- All Tesla and is a release branch;
|
||||
- All Rivian
|
||||
"""
|
||||
assert not self.sla.enabled
|
||||
|
||||
# stay disallowed even when the param may have changed from somewhere else
|
||||
self.params.put("SpeedLimitMode", int(Mode.assist))
|
||||
for _ in range(int(PARAMS_UPDATE_PERIOD / DT_MDL)):
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'],
|
||||
SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert not self.sla.enabled
|
||||
|
||||
def test_disabled(self):
|
||||
self.params.put("SpeedLimitMode", int(Mode.off))
|
||||
for _ in range(int(10. / DT_MDL)):
|
||||
@@ -114,7 +151,7 @@ class TestSpeedLimitAssist:
|
||||
self.sla.state = SpeedLimitAssistState.preActive
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
|
||||
for _ in range(int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)):
|
||||
for _ in range(int(PRE_ACTIVE_GUARD_PERIOD[self.sla.pcm_op_long] / DT_MDL)):
|
||||
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.inactive
|
||||
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
import pytest
|
||||
from cereal import log
|
||||
from cereal import log, custom
|
||||
from openpilot.common.params import Params
|
||||
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
|
||||
|
||||
|
||||
class TurnDirection:
|
||||
none = 0
|
||||
turnLeft = 1
|
||||
turnRight = 2
|
||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||
|
||||
|
||||
@pytest.mark.parametrize("left_blinker,right_blinker,v_ego,blindspot_left,blindspot_right,expected", [
|
||||
(True, False, 5, False, False, TurnDirection.turnLeft),
|
||||
(False, True, 6, False, False, TurnDirection.turnRight),
|
||||
(True, False, 9, False, False, TurnDirection.none),
|
||||
(True, False, 7, True, False, TurnDirection.none),
|
||||
(False, True, 6, False, True, TurnDirection.none),
|
||||
(False, False, 5, False, False, TurnDirection.none),
|
||||
(True, True, 5, False, False, TurnDirection.none),
|
||||
(True, False, 5, False, False, TurnDirection.turnLeft),
|
||||
(False, True, 6, False, False, TurnDirection.turnRight),
|
||||
(True, False, 9, False, False, TurnDirection.none),
|
||||
(True, False, 7, True, False, TurnDirection.none),
|
||||
(False, True, 6, False, True, TurnDirection.none),
|
||||
(False, False, 5, False, False, TurnDirection.none),
|
||||
(True, True, 5, False, False, TurnDirection.none),
|
||||
])
|
||||
def test_lane_turn_desire_conditions(left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected):
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = True
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
|
||||
assert controller.get_turn_direction() == expected
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = True
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
|
||||
assert controller.get_turn_direction() == expected
|
||||
|
||||
|
||||
def test_lane_turn_desire_disabled():
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = False
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
controller.update_lane_turn(False, False, True, False, 7)
|
||||
assert controller.get_turn_direction() == TurnDirection.none
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = False
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
controller.update_lane_turn(False, False, True, False, 7)
|
||||
assert controller.get_turn_direction() == TurnDirection.none
|
||||
|
||||
|
||||
def test_lane_turn_overrides_lane_change():
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = True
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
# left turn desire
|
||||
controller.update_lane_turn(False, False, True, False, 5)
|
||||
assert controller.get_turn_direction() == TurnDirection.turnLeft
|
||||
# right turn desire
|
||||
controller.update_lane_turn(False, False, False, True, 6)
|
||||
assert controller.get_turn_direction() == TurnDirection.turnRight
|
||||
# no turn
|
||||
controller.update_lane_turn(False, False, False, False, 7)
|
||||
assert controller.get_turn_direction() == TurnDirection.none
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = True
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
# left turn desire
|
||||
controller.update_lane_turn(False, False, True, False, 5)
|
||||
assert controller.get_turn_direction() == TurnDirection.turnLeft
|
||||
# right turn desire
|
||||
controller.update_lane_turn(False, False, False, True, 6)
|
||||
assert controller.get_turn_direction() == TurnDirection.turnRight
|
||||
# no turn
|
||||
controller.update_lane_turn(False, False, False, False, 7)
|
||||
assert controller.get_turn_direction() == TurnDirection.none
|
||||
|
||||
|
||||
@pytest.mark.parametrize("v_ego,expected", [
|
||||
(8.93, TurnDirection.turnLeft), # just below threshold
|
||||
(8.96, TurnDirection.none), # above threshold
|
||||
(8.95, TurnDirection.none), # just above threshold
|
||||
(8.93, TurnDirection.turnLeft), # just below threshold
|
||||
(8.96, TurnDirection.none), # above threshold
|
||||
(8.95, TurnDirection.none), # just above threshold
|
||||
])
|
||||
def test_lane_turn_desire_speed_boundary(v_ego, expected):
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = True
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
controller.update_lane_turn(False, True, True, False, v_ego)
|
||||
assert controller.get_turn_direction() == expected
|
||||
dh = DesireHelper()
|
||||
controller = LaneTurnController(dh)
|
||||
controller.enabled = True
|
||||
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
|
||||
controller.turn_direction = TurnDirection.none
|
||||
controller.update_lane_turn(False, True, True, False, v_ego)
|
||||
assert controller.get_turn_direction() == expected
|
||||
|
||||
|
||||
class DummyCarState:
|
||||
def __init__(self, vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
|
||||
steeringPressed=False, steeringTorque=0, brakePressed=False):
|
||||
self.vEgo = vEgo
|
||||
self.leftBlinker = leftBlinker
|
||||
self.rightBlinker = rightBlinker
|
||||
self.leftBlindspot = leftBlindspot
|
||||
self.rightBlindspot = rightBlindspot
|
||||
self.steeringPressed = steeringPressed
|
||||
self.steeringTorque = steeringTorque
|
||||
self.brakePressed = brakePressed
|
||||
def __init__(self, vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
|
||||
steeringPressed=False, steeringTorque=0, brakePressed=False):
|
||||
self.vEgo = vEgo
|
||||
self.leftBlinker = leftBlinker
|
||||
self.rightBlinker = rightBlinker
|
||||
self.leftBlindspot = leftBlindspot
|
||||
self.rightBlindspot = rightBlindspot
|
||||
self.steeringPressed = steeringPressed
|
||||
self.steeringTorque = steeringTorque
|
||||
self.brakePressed = brakePressed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def set_lane_turn_params():
|
||||
params = Params()
|
||||
params.put("LaneTurnDesire", True)
|
||||
params.put("LaneTurnValue", 20.0)
|
||||
params = Params()
|
||||
params.put("LaneTurnDesire", True)
|
||||
params.put("LaneTurnValue", 20.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("carstate, lateral_active, lane_change_prob, expected_desire", [
|
||||
# Lane turn desire overrides lane change desire
|
||||
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnLeft),
|
||||
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnRight),
|
||||
# Lane change desire only (no turn desires)
|
||||
(DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
|
||||
steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft),
|
||||
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
|
||||
steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight),
|
||||
# No desire (inactive)
|
||||
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none),
|
||||
(DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire!
|
||||
# Lane turn desire overrides lane change desire
|
||||
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0,
|
||||
log.Desire.turnLeft),
|
||||
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0,
|
||||
log.Desire.turnRight),
|
||||
# Lane change desire only (no turn desires)
|
||||
(DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
|
||||
steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft),
|
||||
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
|
||||
steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight),
|
||||
# No desire (inactive)
|
||||
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none),
|
||||
(DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire!
|
||||
])
|
||||
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
|
||||
dh = DesireHelper()
|
||||
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||
for _ in range(10):
|
||||
dh.update(carstate, lateral_active, lane_change_prob)
|
||||
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
|
||||
dh = DesireHelper()
|
||||
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||
for _ in range(10):
|
||||
dh.update(carstate, lateral_active, lane_change_prob)
|
||||
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
import platform
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
@@ -12,6 +13,10 @@ from openpilot.common.transformations.coordinates import ecef2geodetic
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
pytest.skip("Skipping locationd test on macOS due to unsupported msgq.", allow_module_level=True)
|
||||
|
||||
|
||||
class TestLocationdProc:
|
||||
LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'liveCalibration',
|
||||
'accelerometer', 'gyroscope', 'magnetometer']
|
||||
|
||||
@@ -4,13 +4,13 @@ from openpilot.common.constants import CV
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \
|
||||
NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType, wrong_car_mode_alert
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target
|
||||
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
AlertStatus = log.SelfdriveState.AlertStatus
|
||||
VisualAlert = car.CarControl.HUDControl.VisualAlert
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
|
||||
EventNameSP = custom.OnroadEventSP.EventName
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messag
|
||||
speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH
|
||||
speed_limit_final_last = sm['longitudinalPlanSP'].speedLimit.resolver.speedLimitFinalLast
|
||||
speed_limit_final_last_conv = round(speed_limit_final_last * speed_conv)
|
||||
alert_1_str = ""
|
||||
alert_2_str = ""
|
||||
alert_size = AlertSize.none
|
||||
|
||||
if CP.openpilotLongitudinalControl and CP.pcmCruise:
|
||||
# PCM long
|
||||
@@ -40,25 +43,16 @@ def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messag
|
||||
pcm_long_required_max = cst_low if speed_limit_final_last_conv < CONFIRM_SPEED_THRESHOLD[metric] else cst_high
|
||||
pcm_long_required_max_set_speed_conv = round(pcm_long_required_max * speed_conv)
|
||||
speed_unit = "km/h" if metric else "mph"
|
||||
|
||||
alert_1_str = "Speed Limit Assist: Activation Required"
|
||||
alert_2_str = f"Manually change set speed to {pcm_long_required_max_set_speed_conv} {speed_unit} to activate"
|
||||
else:
|
||||
# Non PCM long
|
||||
v_cruise_cluster = CS.vCruiseCluster * CV.KPH_TO_MS
|
||||
|
||||
req_plus, req_minus = compare_cluster_target(v_cruise_cluster, speed_limit_final_last, metric)
|
||||
arrow_str = ""
|
||||
if req_plus:
|
||||
arrow_str = "RES/+"
|
||||
elif req_minus:
|
||||
arrow_str = "SET/-"
|
||||
|
||||
alert_2_str = f"Operate the {arrow_str} cruise control button to activate"
|
||||
alert_size = AlertSize.mid
|
||||
|
||||
return Alert(
|
||||
"Speed Limit Assist: Activation Required",
|
||||
alert_1_str,
|
||||
alert_2_str,
|
||||
AlertStatus.normal, AlertSize.mid,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1)
|
||||
AlertStatus.normal, alert_size,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleLow, .1)
|
||||
|
||||
|
||||
class EventsSP(EventsBase):
|
||||
@@ -202,7 +196,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
"Automatically adjusting to the posted speed limit",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.),
|
||||
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
|
||||
},
|
||||
|
||||
EventNameSP.speedLimitChanged: {
|
||||
@@ -210,7 +204,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
"Set speed changed",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.),
|
||||
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
|
||||
},
|
||||
|
||||
EventNameSP.speedLimitPreActive: {
|
||||
@@ -222,7 +216,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
"Automatically adjusting to the last speed limit",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.),
|
||||
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
|
||||
},
|
||||
|
||||
EventNameSP.e2eChime: {
|
||||
@@ -230,6 +224,6 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
"",
|
||||
"",
|
||||
AlertStatus.normal, AlertSize.none,
|
||||
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 0.1),
|
||||
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ from functools import partial
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import set_core_affinity
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \
|
||||
recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler
|
||||
recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler, stat_handler
|
||||
from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException,
|
||||
create_connection, WebSocketConnectionClosedException)
|
||||
|
||||
@@ -33,9 +34,6 @@ SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require
|
||||
DISALLOW_LOG_UPLOAD = threading.Event()
|
||||
|
||||
params = Params()
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
|
||||
|
||||
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||
cloudlog.info("sunnylinkd.handle_long_poll started")
|
||||
@@ -51,7 +49,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||
threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'),
|
||||
threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'),
|
||||
# threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'),
|
||||
# threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'),
|
||||
threading.Thread(target=stat_handler, args=(end_event, Paths.stats_sp_root(), True), name='stat_handler'),
|
||||
] + [
|
||||
threading.Thread(target=jsonrpc_handler, args=(end_event, partial(startLocalProxy, end_event),), name=f'worker_{x}')
|
||||
for x in range(HANDLER_THREADS)
|
||||
@@ -132,6 +130,8 @@ def ws_ping(ws: WebSocket, end_event: threading.Event) -> None:
|
||||
|
||||
|
||||
def ws_queue(end_event: threading.Event) -> None:
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
resume_requested = False
|
||||
tries = 0
|
||||
|
||||
@@ -233,6 +233,9 @@ def saveParams(params_to_update: dict[str, str], compression: bool = False) -> N
|
||||
|
||||
|
||||
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
|
||||
cloudlog.debug("athena.startLocalProxy.starting")
|
||||
ws = create_connection(
|
||||
remote_ws_uri,
|
||||
@@ -254,6 +257,8 @@ def main(exit_event: threading.Event = None):
|
||||
cloudlog.info("Waiting for sunnylink registration to complete")
|
||||
time.sleep(10)
|
||||
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
UploadQueueCache.initialize(upload_queue)
|
||||
|
||||
ws_uri = f"{SUNNYLINK_ATHENA_HOST}"
|
||||
|
||||
278
sunnypilot/sunnylink/statsd.py
Executable file
278
sunnypilot/sunnylink/statsd.py
Executable file
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import zmq
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, UTC
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from cereal.messaging import SubMaster
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.common.file_helpers import atomic_write_in_dir
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S
|
||||
from openpilot.system.statsd import METRIC_TYPE, StatLogSP
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
STATSLOGSP = StatLogSP(intercept=False)
|
||||
|
||||
def sp_stats(end_event):
|
||||
"""Collect sunnypilot-specific statistics and send as raw metrics."""
|
||||
rk = Ratekeeper(.1, print_delay_threshold=None)
|
||||
statlogsp = STATSLOGSP
|
||||
params = Params()
|
||||
|
||||
def flatten_dict(d, parent_key='', sep='.'):
|
||||
items = {}
|
||||
if isinstance(d, dict):
|
||||
for k, v in d.items():
|
||||
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||
items.update(flatten_dict(v, new_key, sep=sep))
|
||||
elif isinstance(d, (list, tuple)):
|
||||
for i, v in enumerate(d):
|
||||
new_key = f"{parent_key}[{i}]"
|
||||
items.update(flatten_dict(v, new_key, sep=sep))
|
||||
else:
|
||||
items[parent_key] = d
|
||||
return items
|
||||
|
||||
# Collect sunnypilot parameters
|
||||
stats_dict = {}
|
||||
|
||||
param_keys = [
|
||||
'SunnylinkEnabled',
|
||||
'AutoLaneChangeBsmDelay',
|
||||
'AutoLaneChangeTimer',
|
||||
'CarPlatformBundle',
|
||||
'CurrentRoute',
|
||||
'DevUIInfo',
|
||||
'EnableCopyparty',
|
||||
'IntelligentCruiseButtonManagement',
|
||||
'QuietMode',
|
||||
'RainbowMode',
|
||||
'ShowAdvancedControls',
|
||||
'Mads',
|
||||
'MadsMainCruiseAllowed',
|
||||
'MadsSteeringMode',
|
||||
'MadsUnifiedEngagementMode',
|
||||
'ModelManager_ActiveBundle',
|
||||
'ModelManager_Favs',
|
||||
'EnableSunnylinkUploader',
|
||||
'SunnylinkEnabled',
|
||||
'InstallDate',
|
||||
'UptimeOffroad',
|
||||
'UptimeOnroad',
|
||||
]
|
||||
|
||||
while not end_event.is_set():
|
||||
try:
|
||||
for key in param_keys:
|
||||
|
||||
try:
|
||||
value = params.get(key)
|
||||
except Exception as e:
|
||||
stats_dict[key] = e
|
||||
continue
|
||||
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
stats_dict.update(flatten_dict(value, key))
|
||||
else:
|
||||
stats_dict[key] = value
|
||||
|
||||
if stats_dict:
|
||||
statlogsp.raw('sunnypilot.device_params', stats_dict)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Exception {e}")
|
||||
finally:
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
def stats_main(end_event):
|
||||
comma_dongle_id = Params().get("DongleId")
|
||||
sunnylink_dongle_id = Params().get("SunnylinkDongleId")
|
||||
|
||||
def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str:
|
||||
res = f"{measurement}"
|
||||
for k, v in tags.items():
|
||||
res += f",{k}={str(v)}"
|
||||
res += " "
|
||||
|
||||
if isinstance(value, float):
|
||||
value = {'value': value}
|
||||
|
||||
for k, v in value.items():
|
||||
res += f"{k}={str(v)},"
|
||||
|
||||
res += f"sunnylink_dongle_id=\"{sunnylink_dongle_id}\",comma_dongle_id=\"{comma_dongle_id}\" {int(timestamp.timestamp() * 1e9)}\n"
|
||||
return res
|
||||
|
||||
def get_influxdb_line_raw(measurement: str, value: dict, timestamp: datetime, tags: dict) -> str:
|
||||
res = f"{measurement}"
|
||||
try:
|
||||
custom_tags = ""
|
||||
for k, v in tags.items():
|
||||
custom_tags += f",{k}={str(v)}"
|
||||
res += custom_tags
|
||||
|
||||
fields = ""
|
||||
for k, v in value.items():
|
||||
# Skip complex types - only keep simple scalar values
|
||||
if isinstance(v, (dict, list, bytes, bytearray)):
|
||||
continue
|
||||
|
||||
fields += f"{k}={json.dumps(v)},"
|
||||
|
||||
res += f" {fields}"
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Unable to get influxdb line for: {value}")
|
||||
res += f",invalid=1 reason={e},"
|
||||
|
||||
res += f"sunnylink_dongle_id=\"{sunnylink_dongle_id}\",comma_dongle_id=\"{comma_dongle_id}\" {int(timestamp.timestamp() * 1e9)}\n"
|
||||
return res
|
||||
|
||||
# open statistics socket
|
||||
ctx = zmq.Context.instance()
|
||||
sock = ctx.socket(zmq.PULL)
|
||||
sock.bind(f"{STATS_SOCKET}_sp")
|
||||
|
||||
STATS_DIR = Paths.stats_sp_root()
|
||||
|
||||
# initialize stats directory
|
||||
Path(STATS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
build_metadata = get_build_metadata()
|
||||
|
||||
# initialize tags
|
||||
tags = {
|
||||
'started': False,
|
||||
'version': build_metadata.openpilot.version,
|
||||
'branch': build_metadata.channel,
|
||||
'dirty': build_metadata.openpilot.is_dirty,
|
||||
'origin': build_metadata.openpilot.git_normalized_origin,
|
||||
'deviceType': HARDWARE.get_device_type(),
|
||||
}
|
||||
|
||||
# subscribe to deviceState for started state
|
||||
sm = SubMaster(['deviceState'])
|
||||
|
||||
idx = 0
|
||||
boot_uid = str(uuid.uuid4())[:8]
|
||||
last_flush_time = time.monotonic()
|
||||
gauges = {}
|
||||
samples: dict[str, list[float]] = defaultdict(list)
|
||||
raws: dict = defaultdict()
|
||||
try:
|
||||
while not end_event.is_set():
|
||||
started_prev = sm['deviceState'].started
|
||||
sm.update()
|
||||
|
||||
# Update metrics
|
||||
while True:
|
||||
try:
|
||||
metric = sock.recv_string(zmq.NOBLOCK)
|
||||
try:
|
||||
metric_type = metric.split('|')[1]
|
||||
metric_name = metric.split(':')[0]
|
||||
metric_value_raw = metric.split('|')[0].split(':')[1]
|
||||
|
||||
if metric_type == METRIC_TYPE.GAUGE:
|
||||
metric_value = float(metric_value_raw)
|
||||
gauges[metric_name] = metric_value
|
||||
elif metric_type == METRIC_TYPE.SAMPLE:
|
||||
metric_value = float(metric_value_raw)
|
||||
samples[metric_name].append(metric_value)
|
||||
elif metric_type == METRIC_TYPE.RAW:
|
||||
raws[metric_name] = metric_value_raw
|
||||
else:
|
||||
cloudlog.event("unknown metric type", metric_type=metric_type)
|
||||
except Exception:
|
||||
print(traceback.format_exc())
|
||||
cloudlog.event("malformed metric", metric=metric)
|
||||
except zmq.error.Again:
|
||||
break
|
||||
|
||||
# flush when started state changes or after FLUSH_TIME_S
|
||||
if (time.monotonic() > last_flush_time + STATS_FLUSH_TIME_S) or (sm['deviceState'].started != started_prev):
|
||||
result = ""
|
||||
current_time = datetime.now(UTC)
|
||||
tags['started'] = sm['deviceState'].started
|
||||
|
||||
for key, value in raws.items():
|
||||
decoded_value = json.loads(base64.b64decode(value).decode('utf-8'))
|
||||
result += get_influxdb_line_raw(key, decoded_value, current_time, tags)
|
||||
|
||||
for key, value in gauges.items():
|
||||
result += get_influxdb_line(f"gauge.{key}", value, current_time, tags)
|
||||
|
||||
for key, values in samples.items():
|
||||
values.sort()
|
||||
sample_count = len(values)
|
||||
sample_sum = sum(values)
|
||||
|
||||
stats = {
|
||||
'count': sample_count,
|
||||
'min': values[0],
|
||||
'max': values[-1],
|
||||
'mean': sample_sum / sample_count,
|
||||
}
|
||||
for percentile in [0.05, 0.5, 0.95]:
|
||||
value = values[int(round(percentile * (sample_count - 1)))]
|
||||
stats[f"p{int(percentile * 100)}"] = value
|
||||
|
||||
result += get_influxdb_line(f"sample.{key}", stats, current_time, tags)
|
||||
|
||||
# clear intermediate data
|
||||
gauges.clear()
|
||||
samples.clear()
|
||||
last_flush_time = time.monotonic()
|
||||
|
||||
# check that we aren't filling up the drive
|
||||
if len(os.listdir(STATS_DIR)) < STATS_DIR_FILE_LIMIT:
|
||||
if len(result) > 0:
|
||||
stats_path = os.path.join(STATS_DIR, f"{boot_uid}_{idx}")
|
||||
with atomic_write_in_dir(stats_path) as f:
|
||||
f.write(result)
|
||||
idx += 1
|
||||
else:
|
||||
cloudlog.error("stats dir full")
|
||||
finally:
|
||||
sock.close()
|
||||
ctx.term()
|
||||
|
||||
|
||||
def main():
|
||||
rk = Ratekeeper(1, print_delay_threshold=None)
|
||||
end_event = threading.Event()
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=stats_main, args=(end_event,)),
|
||||
threading.Thread(target=sp_stats, args=(end_event,)),
|
||||
]
|
||||
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
try:
|
||||
while all(t.is_alive() for t in threads):
|
||||
rk.keep_time()
|
||||
finally:
|
||||
end_event.set()
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -744,26 +744,40 @@ def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None
|
||||
cloudlog.exception("athena.log_handler.exception")
|
||||
|
||||
|
||||
def stat_handler(end_event: threading.Event) -> None:
|
||||
STATS_DIR = Paths.stats_root()
|
||||
def stat_handler(end_event: threading.Event, stats_dir=None, is_sunnylink=False) -> None:
|
||||
stats_dir = stats_dir or Paths.stats_root()
|
||||
last_scan = 0.0
|
||||
|
||||
while not end_event.is_set():
|
||||
curr_scan = time.monotonic()
|
||||
try:
|
||||
if curr_scan - last_scan > 10:
|
||||
stat_filenames = list(filter(lambda name: not name.startswith(tempfile.gettempprefix()), os.listdir(STATS_DIR)))
|
||||
stat_filenames = list(filter(lambda name: not name.startswith(tempfile.gettempprefix()), os.listdir(stats_dir)))
|
||||
if len(stat_filenames) > 0:
|
||||
stat_path = os.path.join(STATS_DIR, stat_filenames[0])
|
||||
stat_path = os.path.join(stats_dir, stat_filenames[0])
|
||||
with open(stat_path) as f:
|
||||
payload = f.read()
|
||||
is_compressed = False
|
||||
|
||||
# Log the current size of the file
|
||||
if is_sunnylink:
|
||||
# Compress and encode the data if it exceeds the maximum size
|
||||
compressed_data = gzip.compress(payload.encode())
|
||||
payload = base64.b64encode(compressed_data).decode()
|
||||
is_compressed = True
|
||||
|
||||
jsonrpc = {
|
||||
"method": "storeStats",
|
||||
"params": {
|
||||
"stats": f.read()
|
||||
"stats": payload
|
||||
},
|
||||
"jsonrpc": "2.0",
|
||||
"id": stat_filenames[0]
|
||||
}
|
||||
|
||||
if is_sunnylink and is_compressed:
|
||||
jsonrpc["params"]["compressed"] = is_compressed
|
||||
|
||||
low_priority_send_queue.put_nowait(json.dumps(jsonrpc))
|
||||
os.remove(stat_path)
|
||||
last_scan = curr_scan
|
||||
|
||||
@@ -55,6 +55,13 @@ class Paths:
|
||||
else:
|
||||
return "/data/stats/"
|
||||
|
||||
@staticmethod
|
||||
def stats_sp_root() -> str:
|
||||
if PC:
|
||||
return str(Path(Paths.comma_home()) / "stats")
|
||||
else:
|
||||
return "/data/stats_sp/"
|
||||
|
||||
@staticmethod
|
||||
def config_root() -> str:
|
||||
if PC:
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "common/swaglog.h"
|
||||
#include "common/version.h"
|
||||
|
||||
#include "sunnypilot/common/version.h"
|
||||
|
||||
// ***** log metadata *****
|
||||
kj::Array<capnp::word> logger_build_init_data() {
|
||||
uint64_t wall_time = nanos_since_epoch();
|
||||
@@ -19,7 +21,7 @@ kj::Array<capnp::word> logger_build_init_data() {
|
||||
auto init = msg.initEvent().initInitData();
|
||||
|
||||
init.setWallTimeNanos(wall_time);
|
||||
init.setVersion(COMMA_VERSION);
|
||||
init.setVersion(SUNNYPILOT_VERSION);
|
||||
init.setDirty(!getenv("CLEAN"));
|
||||
init.setDeviceType(Hardware::get_device_type());
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ def manager_init() -> None:
|
||||
params.put_bool("IsDevelopmentBranch", build_metadata.development_channel)
|
||||
params.put_bool("IsTestedBranch", build_metadata.tested_channel)
|
||||
params.put_bool("IsReleaseBranch", build_metadata.release_channel)
|
||||
params.put_bool("IsReleaseSpBranch", build_metadata.release_sp_channel)
|
||||
params.put("HardwareSerial", serial)
|
||||
|
||||
# set dongle id
|
||||
|
||||
@@ -164,6 +164,7 @@ procs = [
|
||||
# sunnylink <3
|
||||
DaemonProcess("manage_sunnylinkd", "sunnypilot.sunnylink.athena.manage_sunnylinkd", "SunnylinkdPid"),
|
||||
PythonProcess("sunnylink_registration_manager", "sunnypilot.sunnylink.registration_manager", sunnylink_need_register_shim),
|
||||
PythonProcess("statsd_sp", "sunnypilot.sunnylink.statsd", and_(always_run, sunnylink_ready_shim)),
|
||||
]
|
||||
|
||||
# sunnypilot
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
import zmq
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, UTC
|
||||
from datetime import datetime, UTC, date
|
||||
from typing import NoReturn
|
||||
|
||||
from openpilot.common.params import Params
|
||||
@@ -21,18 +25,21 @@ from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET,
|
||||
class METRIC_TYPE:
|
||||
GAUGE = 'g'
|
||||
SAMPLE = 'sa'
|
||||
RAW = 'r'
|
||||
|
||||
|
||||
class StatLog:
|
||||
def __init__(self):
|
||||
self.pid = None
|
||||
self.zctx = None
|
||||
self.sock = None
|
||||
self.stats_socket = STATS_SOCKET
|
||||
|
||||
def connect(self) -> None:
|
||||
self.zctx = zmq.Context()
|
||||
self.zctx = zmq.Context.instance() or zmq.Context()
|
||||
self.sock = self.zctx.socket(zmq.PUSH)
|
||||
self.sock.setsockopt(zmq.LINGER, 10)
|
||||
self.sock.connect(STATS_SOCKET)
|
||||
self.sock.connect(self.stats_socket)
|
||||
self.pid = os.getpid()
|
||||
|
||||
def __del__(self):
|
||||
@@ -60,6 +67,50 @@ class StatLog:
|
||||
self._send(f"{name}:{value}|{METRIC_TYPE.SAMPLE}")
|
||||
|
||||
|
||||
class StatLogSP(StatLog):
|
||||
def __init__(self, intercept=True):
|
||||
"""
|
||||
Initializes the class instance with an optional parameter to determine
|
||||
if statistical logging should be configured or not.
|
||||
|
||||
:param intercept: A boolean flag that indicates whether to initialize
|
||||
the `comma_statlog`. If True, the `comma_statlog` attribute is
|
||||
instantiated as a `StatLog` object. Defaults to True.
|
||||
"""
|
||||
super().__init__()
|
||||
self.comma_statlog = StatLog() if intercept else None
|
||||
self.stats_socket = f"{STATS_SOCKET}_sp"
|
||||
|
||||
def connect(self) -> None:
|
||||
super().connect()
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog.connect()
|
||||
|
||||
def __del__(self):
|
||||
super().__del__()
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog.__del__()
|
||||
|
||||
def _send(self, metric: str) -> None:
|
||||
super()._send(metric)
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog._send(metric)
|
||||
|
||||
@staticmethod
|
||||
def default_converter(obj):
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.isoformat()
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
if isinstance(obj, Decimal):
|
||||
return float(obj)
|
||||
return str(obj) # fallback for unknown types
|
||||
|
||||
def raw(self, name: str, value: dict) -> None:
|
||||
encoded_dict = base64.b64encode(json.dumps(value, default=self.default_converter).encode("utf-8")).decode("utf-8")
|
||||
self._send(f"{name}:{encoded_dict}|{METRIC_TYPE.RAW}")
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
dongle_id = Params().get("DongleId")
|
||||
def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str:
|
||||
@@ -180,4 +231,4 @@ def main() -> NoReturn:
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
statlog = StatLog()
|
||||
statlog = StatLogSP(intercept=True)
|
||||
|
||||
@@ -82,7 +82,7 @@ def set_consistent_flag(consistent: bool) -> None:
|
||||
|
||||
def parse_release_notes(basedir: str) -> bytes:
|
||||
try:
|
||||
with open(os.path.join(basedir, "RELEASES.md"), "rb") as f:
|
||||
with open(os.path.join(basedir, "CHANGELOG.md"), "rb") as f:
|
||||
r = f.read().split(b'\n\n', 1)[0] # Slice latest release notes
|
||||
try:
|
||||
return bytes(parse_markdown(r.decode("utf-8")), encoding="utf-8")
|
||||
@@ -294,7 +294,7 @@ class Updater:
|
||||
try:
|
||||
branch = self.get_branch(basedir)
|
||||
commit = self.get_commit_hash(basedir)[:7]
|
||||
with open(os.path.join(basedir, "common", "version.h")) as f:
|
||||
with open(os.path.join(basedir, "sunnypilot", "common", "version.h")) as f:
|
||||
version = f.read().split('"')[1]
|
||||
|
||||
commit_unix_ts = run(["git", "show", "-s", "--format=%ct", "HEAD"], basedir).rstrip()
|
||||
|
||||
@@ -10,11 +10,11 @@ from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date
|
||||
|
||||
RELEASE_SP_BRANCHES = ['release-c3', 'release']
|
||||
RELEASE_SP_BRANCHES = ['release-c3', 'release', 'release-tizi', 'release-tici', 'release-tizi-staging', 'release-tici-staging']
|
||||
TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new', 'staging']
|
||||
MASTER_SP_BRANCHES = ['master']
|
||||
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES
|
||||
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES
|
||||
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly']
|
||||
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + RELEASE_SP_BRANCHES + TESTED_SP_BRANCHES
|
||||
|
||||
SP_BRANCH_MIGRATIONS = {
|
||||
("tici", "staging-c3-new"): "staging-tici",
|
||||
@@ -33,13 +33,13 @@ terms_version: str = "2"
|
||||
|
||||
|
||||
def get_version(path: str = BASEDIR) -> str:
|
||||
with open(os.path.join(path, "common", "version.h")) as _versionf:
|
||||
with open(os.path.join(path, "sunnypilot", "common", "version.h")) as _versionf:
|
||||
version = _versionf.read().split('"')[1]
|
||||
return version
|
||||
|
||||
|
||||
def get_release_notes(path: str = BASEDIR) -> str:
|
||||
with open(os.path.join(path, "RELEASES.md")) as f:
|
||||
with open(os.path.join(path, "CHANGELOG.md")) as f:
|
||||
return f.read().split('\n\n', 1)[0]
|
||||
|
||||
|
||||
@@ -96,7 +96,9 @@ class OpenpilotMetadata:
|
||||
@property
|
||||
def sunnypilot_remote(self) -> bool:
|
||||
return self.git_normalized_origin in ("github.com/sunnypilot/sunnypilot",
|
||||
"github.com/sunnypilot/openpilot")
|
||||
"github.com/sunnypilot/openpilot",
|
||||
"github.com/sunnyhaibin/sunnypilot",
|
||||
"github.com/sunnyhaibin/openpilot")
|
||||
|
||||
@property
|
||||
def git_normalized_origin(self) -> str:
|
||||
@@ -120,6 +122,10 @@ class BuildMetadata:
|
||||
def release_channel(self) -> bool:
|
||||
return self.channel in RELEASE_BRANCHES
|
||||
|
||||
@property
|
||||
def release_sp_channel(self) -> bool:
|
||||
return self.channel in RELEASE_SP_BRANCHES
|
||||
|
||||
@property
|
||||
def canonical(self) -> str:
|
||||
return f"{self.openpilot.version}-{self.openpilot.git_commit}-{self.openpilot.build_style}"
|
||||
@@ -146,7 +152,7 @@ class BuildMetadata:
|
||||
return "staging"
|
||||
elif self.master_channel:
|
||||
return "master"
|
||||
elif self.release_channel:
|
||||
elif self.release_channel or self.release_sp_channel:
|
||||
return "release"
|
||||
else:
|
||||
return "feature"
|
||||
|
||||
676
tools/clip/clippy.py
Normal file
676
tools/clip/clippy.py
Normal file
@@ -0,0 +1,676 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import html
|
||||
from collections import deque
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
from openpilot.tools.lib.api import CommaApi, UnauthorizedError
|
||||
from openpilot.tools.lib.route import Route
|
||||
|
||||
import threading
|
||||
from flask import Flask, send_file, abort, make_response
|
||||
from pathlib import Path
|
||||
|
||||
if not (CLIPPY_TOKEN := os.getenv("CLIPPY_TOKEN")):
|
||||
sys.exit("❌ CLIPPY_TOKEN is missing – set it in the environment")
|
||||
|
||||
ALLOWED_GUILD_IDS = {880416502577266699, 1368811404689276958}
|
||||
|
||||
CLIPPY_BASE_URL = "https://clippy.royjr.com"
|
||||
|
||||
WORKING_DIR = os.path.expanduser("~/github/sunnypilot/tools/clip")
|
||||
CLIPS_DIR = os.path.join(WORKING_DIR, "clips")
|
||||
STATS_PATH = os.path.join(WORKING_DIR, "clippy_stats.json")
|
||||
LOG_PATH = os.path.join(WORKING_DIR, "clippy.log")
|
||||
os.makedirs(CLIPS_DIR, exist_ok=True)
|
||||
|
||||
MAX_TOTAL_JOBS = 20
|
||||
MAX_CONCURRENT_CLIPS = 3
|
||||
MAX_CONCURRENT_CLIPS_PER_USER = 3
|
||||
MAX_CLIP_DURATION = 60 * 5
|
||||
|
||||
CLIPPY_STATS_ALLOWED_ROLES = ["sunnypilot-dev"]
|
||||
CLIPPY_UNLIMITED_ALLOWED_ROLES = ["sunnypilot-dev"]
|
||||
|
||||
TAIL_LINES = 25
|
||||
tail_buffer = deque(maxlen=TAIL_LINES)
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
bot = commands.Bot(command_prefix=lambda bot, msg: [], intents=intents)
|
||||
|
||||
clip_queue = []
|
||||
clip_semaphore = asyncio.Semaphore(MAX_CONCURRENT_CLIPS)
|
||||
user_cooldowns = {}
|
||||
|
||||
|
||||
async def queue_monitor():
|
||||
while True:
|
||||
print("\033c", end="")
|
||||
w = shutil.get_terminal_size().columns
|
||||
bar = "-" * w
|
||||
print(f"{bar}\nTotal: {stats['total']} | ✅ {stats['success']} | ❌ {stats['fail']}\n{bar}")
|
||||
print("\n".join(f"{i+1:02d}. {j['status']} {j['user']}: {j['route']}" for i, j in enumerate(clip_queue)) or "No jobs in queue.")
|
||||
print(f"{bar}\n" + "\n".join(line[:w] for line in tail_buffer) + f"\n{bar}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def start_clip_server():
|
||||
clip_dir_resolved = Path(CLIPS_DIR).resolve()
|
||||
app = Flask("clippy")
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def get_clip(filename):
|
||||
full_path = (clip_dir_resolved / filename).resolve()
|
||||
try:
|
||||
full_path.relative_to(clip_dir_resolved)
|
||||
except ValueError:
|
||||
abort(404)
|
||||
if not full_path.name.endswith(".mp4"):
|
||||
abort(404)
|
||||
try:
|
||||
if not full_path.is_file() or not full_path.samefile(full_path):
|
||||
abort(404)
|
||||
except Exception:
|
||||
abort(404)
|
||||
response = make_response(send_file(
|
||||
str(full_path),
|
||||
mimetype="video/mp4",
|
||||
as_attachment=False,
|
||||
conditional=True,
|
||||
))
|
||||
response.headers.update({
|
||||
"Cache-Control": "no-store",
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Disposition": f'inline; filename="{filename}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
})
|
||||
return response
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(_):
|
||||
return "clip not found", 404
|
||||
|
||||
app.run(host="127.0.0.1", port=5000)
|
||||
|
||||
def has_any_role(user, role_list):
|
||||
if isinstance(user, discord.Member):
|
||||
return any(role.name in role_list for role in user.roles)
|
||||
return False
|
||||
|
||||
def user_tag(user: discord.User) -> str:
|
||||
return f"{user.display_name} ({user.name})"
|
||||
|
||||
def load_stats():
|
||||
if os.path.exists(STATS_PATH):
|
||||
with open(STATS_PATH, "r") as f:
|
||||
return json.load(f)
|
||||
return {"total": 0, "success": 0, "fail": 0}
|
||||
|
||||
def save_stats():
|
||||
with open(STATS_PATH, "w") as f:
|
||||
json.dump(stats, f)
|
||||
|
||||
stats = load_stats()
|
||||
|
||||
|
||||
class SanitizeFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
if isinstance(record.msg, str):
|
||||
record.msg = re.compile(r'[\x00-\x1f\x7f-\x9f]').sub('', record.msg)
|
||||
return True
|
||||
|
||||
|
||||
class DequeHandler(logging.Handler):
|
||||
def __init__(self, buf):
|
||||
super().__init__()
|
||||
self.buf = buf
|
||||
self.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
self.buf.append(self.format(record))
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
log_handler = RotatingFileHandler(LOG_PATH, maxBytes=5*1024*1024, backupCount=3)
|
||||
log_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.INFO)
|
||||
root.addHandler(log_handler)
|
||||
root.addHandler(DequeHandler(tail_buffer))
|
||||
root.addFilter(SanitizeFilter())
|
||||
|
||||
|
||||
class DeletePublishedView(discord.ui.View):
|
||||
def __init__(self, message: discord.Message, author_id: int, video_path):
|
||||
super().__init__(timeout=300)
|
||||
self.message = message
|
||||
self.author_id = author_id
|
||||
self.video_path = video_path
|
||||
|
||||
@discord.ui.button(label="Unpublish Clip", style=discord.ButtonStyle.primary)
|
||||
async def unpublish(self, interaction: discord.Interaction, _button: discord.ui.Button):
|
||||
if interaction.user.id != self.author_id:
|
||||
logging.error(f"🚫 {user_tag(interaction.user)} cant unpublish {self.message.id}")
|
||||
await interaction.response.send_message("🚫 You can't unpublish this clip.", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
await self.message.delete()
|
||||
logging.info(f"🗑️ {user_tag(interaction.user)} unpublished {self.message.id}")
|
||||
await interaction.response.edit_message(content="🗑️ Unpublished clip.", view=None)
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Failed to unpublish clip {self.message.id}: {e}")
|
||||
await interaction.response.send_message(f"❌ Failed to unpublish clip.", ephemeral=True)
|
||||
|
||||
@discord.ui.button(label="Unpublish + Delete Clip", style=discord.ButtonStyle.danger)
|
||||
async def delete(self, interaction: discord.Interaction, _button: discord.ui.Button):
|
||||
if interaction.user.id != self.author_id:
|
||||
logging.error(f"🚫 {user_tag(interaction.user)} cant unpublish and delete {self.message.id}")
|
||||
await interaction.response.send_message("🚫 You can't unpublish and delete this clip.", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
if not os.path.realpath(self.video_path).startswith(os.path.realpath(CLIPS_DIR) + os.sep):
|
||||
logging.error(f"❌ Unsafe delete attempt: {self.video_path}")
|
||||
await interaction.response.send_message("❌ Unsafe delete attempt.", ephemeral=True)
|
||||
return
|
||||
os.remove(self.video_path)
|
||||
await self.message.delete()
|
||||
logging.info(f"🗑️ {user_tag(interaction.user)} unpublished {self.message.id}")
|
||||
await interaction.response.edit_message(content="🗑️ Unpublished and deleted clip.", view=None)
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Failed to unpublish clip {self.message.id}: {e}")
|
||||
await interaction.response.send_message(f"❌ Failed to unpublish and delete clip.", ephemeral=True)
|
||||
|
||||
|
||||
class PublishView(discord.ui.View):
|
||||
def __init__(self, route_str, title, video_path, author_id, file_size, safe_name):
|
||||
super().__init__(timeout=300)
|
||||
self.route_str = route_str
|
||||
self.title = title
|
||||
self.video_path = video_path
|
||||
self.author_id = author_id
|
||||
self.file_size = file_size
|
||||
self.safe_name = safe_name
|
||||
|
||||
@discord.ui.button(label="Publish Clip", style=discord.ButtonStyle.success)
|
||||
async def publish(self, interaction: discord.Interaction, _button: discord.ui.Button):
|
||||
if interaction.user.id != self.author_id:
|
||||
await interaction.response.send_message("🚫 You can't publish this clip.", ephemeral=True)
|
||||
return
|
||||
|
||||
if not os.path.exists(self.video_path):
|
||||
logging.error(f"❌ {user_tag(interaction.user)} failed to publish {self.route_str} – file missing")
|
||||
await interaction.response.edit_message(
|
||||
content="❌ Clip could not be published. File missing.",
|
||||
attachments=[], view=None
|
||||
)
|
||||
self.stop()
|
||||
return
|
||||
|
||||
logging.info(f"✅ {user_tag(interaction.user)} published {self.route_str}")
|
||||
|
||||
if not (1 <= self.file_size <= 9):
|
||||
published_msg = await interaction.channel.send(
|
||||
f"{interaction.user.mention} shared a [clip]({CLIPPY_BASE_URL}/{self.safe_name}.mp4) from [{self.route_str}](https://connect.comma.ai/{self.route_str})\n{self.title}"
|
||||
)
|
||||
else:
|
||||
published_msg = await interaction.channel.send(
|
||||
f"{interaction.user.mention} shared a clip from [{self.route_str}](https://connect.comma.ai/{self.route_str})\n{self.title}",
|
||||
file=discord.File(self.video_path)
|
||||
)
|
||||
await interaction.response.edit_message(
|
||||
content="✅ Clip published to channel.", attachments=[], view=DeletePublishedView(published_msg, interaction.user.id, self.video_path),
|
||||
)
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Delete Clip", style=discord.ButtonStyle.danger)
|
||||
async def delete(self, interaction: discord.Interaction, _button: discord.ui.Button):
|
||||
if interaction.user.id != self.author_id:
|
||||
logging.error(f"🚫 {user_tag(interaction.user)} cant delete {self.video_path}")
|
||||
await interaction.response.send_message("🚫 You can't delete this clip.", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
if not os.path.realpath(self.video_path).startswith(os.path.realpath(CLIPS_DIR) + os.sep):
|
||||
logging.error(f"❌ Unsafe delete attempt: {self.video_path}")
|
||||
await interaction.response.send_message("❌ Unsafe delete attempt.", ephemeral=True)
|
||||
return
|
||||
os.remove(self.video_path)
|
||||
logging.info(f"🗑️ {user_tag(interaction.user)} deleted {self.route_str}")
|
||||
await interaction.response.edit_message(content="🗑️ Clip deleted.", attachments=[], view=None)
|
||||
self.stop()
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Failed to delete {self.route_str}")
|
||||
await interaction.response.edit_message(content=f"❌ Failed to delete clip.", view=None)
|
||||
|
||||
|
||||
@bot.tree.command(name="clippy", description="Generate a driving clip - make sure you upload logs first!")
|
||||
@app_commands.describe(
|
||||
input="connect link or dongle/route/starttime/endtime or dongle/route/startsegment-endsegment",
|
||||
title="Title (default: none)",
|
||||
quality="Video quality (default: high)",
|
||||
wide="Use wide view if uploaded (default: true)",
|
||||
speed="Playback speed (default: 1)",
|
||||
cache="Set to false to regenerate clip if its already cached (default: true)",
|
||||
private="If true, only you will see the preview (default: true)",
|
||||
bookmarks="Automatically clip bookmarks (default: false)",
|
||||
filesize="Max filesize (MB), set to 0 for unlimited (default: 9)",
|
||||
developer="Show the developer UI (default: Off)"
|
||||
)
|
||||
@app_commands.choices(
|
||||
quality=[
|
||||
app_commands.Choice(name="high", value="high"),
|
||||
app_commands.Choice(name="low", value="low"),
|
||||
],
|
||||
developer=[
|
||||
app_commands.Choice(name="Right", value="1"),
|
||||
app_commands.Choice(name="Right & Bottom", value="2"),
|
||||
]
|
||||
)
|
||||
async def clippy(
|
||||
interaction: discord.Interaction,
|
||||
input: str,
|
||||
title: str = None,
|
||||
quality: app_commands.Choice[str] | None = None,
|
||||
wide: bool = True,
|
||||
speed: int = 1,
|
||||
cache: bool = True,
|
||||
private: bool = True,
|
||||
bookmarks: bool = False,
|
||||
filesize: int = 9,
|
||||
developer: app_commands.Choice[str] | None = None,
|
||||
):
|
||||
|
||||
if interaction.guild_id not in ALLOWED_GUILD_IDS:
|
||||
logging.error(f"❌ This bot is not available in this server {interaction.guild_id}")
|
||||
await interaction.response.send_message("❌ This bot is not available in this server.", ephemeral=True)
|
||||
return
|
||||
|
||||
if len(clip_queue) >= MAX_TOTAL_JOBS:
|
||||
await interaction.response.send_message(
|
||||
"🚫 Server busy – too many jobs in queue. Please try again later.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
user_id = interaction.user.id
|
||||
if not has_any_role(interaction.user, CLIPPY_UNLIMITED_ALLOWED_ROLES):
|
||||
if user_cooldowns.get(user_id, 0) >= MAX_CONCURRENT_CLIPS_PER_USER:
|
||||
logging.error(f"🚫 {user_tag(interaction.user)} hit the cooldown limit")
|
||||
await interaction.response.send_message(
|
||||
"🚫 You already have a clip running. Wait for it to finish.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
user_cooldowns[user_id] = user_cooldowns.get(user_id, 0) + 1
|
||||
|
||||
try:
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
quality_value = quality.value if quality else "high"
|
||||
title_cmd = title[:80] if title else ""
|
||||
title = f"> ### **{html.unescape(title[:80])}**" if title else ""
|
||||
stats["total"] += 1
|
||||
|
||||
# ── fast‑fail validation ────────────────────────────────────────────────────
|
||||
def fail(msg: str):
|
||||
stats["fail"] += 1
|
||||
save_stats()
|
||||
return interaction.followup.send(f"❌ {msg}", ephemeral=True)
|
||||
|
||||
input = input.removeprefix("https://connect.comma.ai/")
|
||||
|
||||
if bookmarks:
|
||||
match = re.match(r'^([a-z0-9]+)/([a-zA-Z0-9\-]+)$', input)
|
||||
if not match:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} entered bad input {input}")
|
||||
await fail("Use connect link, `dongle/route/starttime/endtime` or `dongle/route/startsegment-endsegment` (endsegment optional).\n```\n--- CONNECT ---\nhttps://connect.comma.ai/a2a0ccea32023010/2023-07-27--13-01-19/5/10\n\n--- EXAMPLES ---\na2a0ccea32023010/2023-07-27--13-01-19/0 segment 0\na2a0ccea32023010/2023-07-27--13-01-19/0-1 segments 0 through 1\na2a0ccea32023010/2023-07-27--13-01-19/5/10 from 5 to 10 seconds\na2a0ccea32023010/2023-07-27--13-01-19 when using bookmark option\n```")
|
||||
return
|
||||
else:
|
||||
dongle, route = match.groups()
|
||||
start = 0
|
||||
end = 0
|
||||
else:
|
||||
|
||||
match = re.match(r'^([a-z0-9]+)/([a-zA-Z0-9\-]+)/(\d+)/(\d+)$', input)
|
||||
if not match:
|
||||
match = re.match(r"^([a-z0-9]+)/([A-Za-z0-9\-]+)/(\d+)(?:-(\d+))?$", input)
|
||||
if not match:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} entered bad input {input}")
|
||||
await fail("Use connect link, `dongle/route/starttime/endtime` or `dongle/route/startsegment-endsegment` (endsegment optional).\n```\n--- CONNECT ---\nhttps://connect.comma.ai/a2a0ccea32023010/2023-07-27--13-01-19/5/10\n\n--- EXAMPLES ---\na2a0ccea32023010/2023-07-27--13-01-19/0 segment 0\na2a0ccea32023010/2023-07-27--13-01-19/0-1 segments 0 through 1\na2a0ccea32023010/2023-07-27--13-01-19/5/10 from 5 to 10 seconds\na2a0ccea32023010/2023-07-27--13-01-19 when using bookmark option\n```")
|
||||
return
|
||||
else:
|
||||
dongle, route, seg_start, seg_end = match.groups()
|
||||
|
||||
if int(seg_start) == 0:
|
||||
# in_start = 2 # fix for 2s
|
||||
in_start = 0
|
||||
else:
|
||||
in_start = int(seg_start) * 60
|
||||
|
||||
if seg_end is None:
|
||||
in_end = 60 if int(seg_start) == 0 else in_start + 60
|
||||
else:
|
||||
in_end = 60 if int(seg_end) == 0 else (int(seg_end) + 1) * 60
|
||||
else:
|
||||
dongle, route, in_start, in_end = match.groups()
|
||||
|
||||
start = int(in_start)
|
||||
end = int(in_end)
|
||||
|
||||
# fix for 2s
|
||||
# if start < 2 or end <= start:
|
||||
# await fail("Start must be at least 2 and end must be greater than start.")
|
||||
# return
|
||||
|
||||
if end <= start:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} entered bad times {input}")
|
||||
await fail("End must be greater than start time.")
|
||||
return
|
||||
duration = end - start
|
||||
if duration > MAX_CLIP_DURATION:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} hit the max duration limit {input}")
|
||||
await fail(f"Clips must be {int(MAX_CLIP_DURATION / 60)} minutes or less.")
|
||||
return
|
||||
|
||||
status_msg = await interaction.followup.send(
|
||||
"🕐 Waiting in queue..", ephemeral=private
|
||||
)
|
||||
|
||||
if speed == 0:
|
||||
speed = 1
|
||||
if speed > 1:
|
||||
end = start + int(duration / speed)
|
||||
elif speed < 1:
|
||||
end = start + int(duration / speed)
|
||||
if bookmarks:
|
||||
route_str = f"{dongle}/{route}"
|
||||
connect_route_str = f"{dongle}/{route}"
|
||||
base = f"{dongle}_{route}_bookmarks_{quality_value}"
|
||||
else:
|
||||
route_str = f"{dongle}/{route}/{start}/{end}"
|
||||
connect_start = 1 if start == 0 else start
|
||||
connect_route_str = f"{dongle}/{route}/{connect_start}/{end}"
|
||||
base = f"{dongle}_{route}_{start}_{end}_{quality_value}"
|
||||
if wide:
|
||||
base += "_wide"
|
||||
if speed:
|
||||
base += f"_{speed}"
|
||||
base += f"_s{filesize}"
|
||||
clean_base = re.sub(r'[^A-Za-z0-9_-]+', '_', base)
|
||||
if title_cmd:
|
||||
title_hash = hashlib.sha1(title_cmd.encode()).hexdigest()[:10]
|
||||
safe_name = f"{clean_base}_{title_hash}"
|
||||
else:
|
||||
safe_name = clean_base
|
||||
|
||||
safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', safe_name)
|
||||
|
||||
if any(job["route"] == safe_name for job in clip_queue):
|
||||
await status_msg.edit(content="❌ That clip is already in the queue or processing – wait for it to finish.")
|
||||
return
|
||||
|
||||
try:
|
||||
logs = CommaApi().get(f"/v1/route/{dongle}|{route}/files").get("logs")
|
||||
|
||||
segments = [
|
||||
re.search(r'/(\d+)/rlog\.(?:zst|bz2)', url).group(1)
|
||||
for url in logs
|
||||
if re.search(r'/(\d+)/rlog\.(?:zst|bz2)', url)
|
||||
]
|
||||
|
||||
startsegment = start // 60
|
||||
endsegment = (end - 1) // 60
|
||||
|
||||
segment_set = set(int(s) for s in segments)
|
||||
if bookmarks:
|
||||
missing = False
|
||||
else:
|
||||
missing = [i for i in range(startsegment, endsegment + 1) if i not in segment_set]
|
||||
|
||||
if missing:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} segments missing {missing}")
|
||||
await status_msg.edit(content=f"❌ You need to upload the missing logs for segments `{missing}` using [connect.comma.ai](https://connect.comma.ai/{connect_route_str})")
|
||||
return
|
||||
else:
|
||||
if bookmarks:
|
||||
logging.info(f"🕐 {user_tag(interaction.user)} getting bookmarks {route_str}")
|
||||
await status_msg.edit(content=f"🕐 Getting bookmarks")
|
||||
else:
|
||||
logging.info(f"☑️ {user_tag(interaction.user)} segments present {route_str}")
|
||||
await status_msg.edit(content=f"☑️ All required segments are present.")
|
||||
|
||||
except UnauthorizedError as e:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} unauthorized: {e}")
|
||||
await status_msg.edit(content=f"❌ You need to make the route public using [connect.comma.ai](https://connect.comma.ai/{route_str}). `/clippy-auth` is no longer supported.")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} unexpected error: {e}")
|
||||
await status_msg.edit(content=f"❌ Error: unexpected error")
|
||||
return
|
||||
|
||||
if bookmarks:
|
||||
try:
|
||||
route = Route(route_str)
|
||||
user_flags_at_time = []
|
||||
|
||||
for segment in route.segments:
|
||||
for event in segment.events:
|
||||
if event['type'] == 'user_flag':
|
||||
user_flags_at_time.append(round(event['route_offset_millis'] / 1000))
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} unauthorized: {e}")
|
||||
await status_msg.edit(content=f"❌ You need to make the route public using [connect.comma.ai](https://connect.comma.ai/{route_str}). `/clippy-auth` is no longer supported.")
|
||||
return
|
||||
|
||||
if len(user_flags_at_time) == 0:
|
||||
logging.error(f"❌ {user_tag(interaction.user)} no bookmarks found")
|
||||
await status_msg.edit(content=f"❌ No bookmarks found")
|
||||
return
|
||||
else:
|
||||
bookmarklinks = ''
|
||||
for user_flag_at_time in user_flags_at_time:
|
||||
bookmarklinks += f"```{connect_route_str}/{user_flag_at_time - 10}/{user_flag_at_time + 5}```"
|
||||
logging.info(f"✅ {user_tag(interaction.user)} {len(user_flags_at_time)} bookmarks found! - {user_flags_at_time}")
|
||||
await status_msg.edit(content=f"✅ {len(user_flags_at_time)} bookmarks found! - {user_flags_at_time}{bookmarklinks}")
|
||||
return
|
||||
|
||||
full_path = os.path.join(CLIPS_DIR, f"{safe_name}.mp4")
|
||||
|
||||
clip_queue.append({"user": interaction.user.display_name,
|
||||
"route": safe_name,
|
||||
"duration": duration,
|
||||
"status": "🕐"})
|
||||
save_stats()
|
||||
if private:
|
||||
logging.info(f"🕐 {user_tag(interaction.user)} queued (PRIVATE) {route_str}")
|
||||
else:
|
||||
logging.info(f"🕐 {user_tag(interaction.user)} queued {route_str}")
|
||||
|
||||
if os.path.exists(full_path) and cache:
|
||||
stats["success"] += 1
|
||||
save_stats()
|
||||
for j in clip_queue:
|
||||
if j["route"] == safe_name:
|
||||
j["status"] = "✅"
|
||||
|
||||
if private:
|
||||
logging.info(f"📁 {user_tag(interaction.user)} used cache (PRIVATE) {route_str}")
|
||||
await status_msg.edit(content="📁 Used cached clip.")
|
||||
if not (1 <= filesize <= 9):
|
||||
await interaction.followup.send(
|
||||
content=f"Preview for [`{route_str}`]({CLIPPY_BASE_URL}/{safe_name}.mp4)\n{title}",
|
||||
view=PublishView(route_str, title, full_path, interaction.user.id, filesize, safe_name),
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.followup.send(
|
||||
content=f"Preview for `{route_str}`\n{title}",
|
||||
file=discord.File(full_path),
|
||||
view=PublishView(route_str, title, full_path, interaction.user.id, filesize, safe_name),
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
logging.info(f"📁 {user_tag(interaction.user)} used cache {route_str}")
|
||||
|
||||
if not (1 <= filesize <= 9):
|
||||
published_msg = await interaction.channel.send(
|
||||
f"{interaction.user.mention} shared a [clip]({CLIPPY_BASE_URL}/{safe_name}.mp4) from [{route_str}](https://connect.comma.ai/{route_str})\n{title}"
|
||||
)
|
||||
else:
|
||||
published_msg = await interaction.channel.send(
|
||||
f"{interaction.user.mention} shared a clip from [{route_str}](https://connect.comma.ai/{route_str})\n{title}",
|
||||
file=discord.File(full_path)
|
||||
)
|
||||
await status_msg.edit(
|
||||
content="📁 Used cached clip.", attachments=[], view=DeletePublishedView(published_msg, interaction.user.id, full_path),
|
||||
)
|
||||
|
||||
else:
|
||||
async with clip_semaphore:
|
||||
for j in clip_queue:
|
||||
if j["route"] == safe_name:
|
||||
j["status"] = "🔄"
|
||||
logging.info(f"🔄 {user_tag(interaction.user)} processing {route_str}")
|
||||
await status_msg.edit(content=f"🔄 Processing {j['duration']}s clip..")
|
||||
|
||||
cmd = ["python3", "run.py", route_str, "-q", quality_value, "-x", str(speed), "-o", full_path]
|
||||
if not (in_start and in_end):
|
||||
if in_start != 0: # fix for 2s
|
||||
cmd += ["-s", str(start), "-e", str(end)]
|
||||
|
||||
if title_cmd:
|
||||
cmd += ["-t", str(title_cmd)]
|
||||
|
||||
if wide:
|
||||
cmd += ["-w"]
|
||||
|
||||
if filesize:
|
||||
cmd += ["-f", str(filesize)]
|
||||
|
||||
if developer:
|
||||
dev_mode = int(developer.value)
|
||||
else:
|
||||
dev_mode = 0
|
||||
cmd += ["-z", str(dev_mode)]
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd, cwd=WORKING_DIR,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
clean_err = "\n".join(stderr.decode().splitlines()[3:]) if stderr else ""
|
||||
|
||||
if proc.returncode != 0 or not os.path.exists(full_path):
|
||||
for j in clip_queue:
|
||||
if j["route"] == safe_name:
|
||||
j["status"] = "❌"
|
||||
stats["fail"] += 1
|
||||
save_stats()
|
||||
logging.error(f"❌ {user_tag(interaction.user)} failed {route_str}\n{clean_err}")
|
||||
|
||||
if clean_err == "clip.py: error: failed to get route: Unauthorized. Authenticate with tools/lib/auth.py":
|
||||
await status_msg.edit(content=f"❌ You need to make the route public using [connect.comma.ai](https://connect.comma.ai/{route_str}). `/clippy-auth` is no longer supported.")
|
||||
elif clean_err == "clip.py: error: failed to get route: 404:The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.":
|
||||
await status_msg.edit(content="❌ This route does not exist, please try another.")
|
||||
else:
|
||||
await status_msg.edit(content="❌ Clip failed to generate.")
|
||||
else:
|
||||
for j in clip_queue:
|
||||
if j["route"] == safe_name:
|
||||
j["status"] = "✅"
|
||||
stats["success"] += 1
|
||||
save_stats()
|
||||
|
||||
if private:
|
||||
logging.info(f"✅ {user_tag(interaction.user)} success (PRIVATE) {route_str}")
|
||||
await status_msg.edit(content="✅ Clip ready.")
|
||||
|
||||
if not (1 <= filesize <= 9):
|
||||
await interaction.followup.send(
|
||||
content=f"Preview for [`{route_str}`]({CLIPPY_BASE_URL}/{safe_name}.mp4)\n{title}",
|
||||
view=PublishView(route_str, title, full_path, interaction.user.id, filesize, safe_name),
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.followup.send(
|
||||
content=f"Preview for `{route_str}`\n{title}",
|
||||
file=discord.File(full_path),
|
||||
view=PublishView(route_str, title, full_path, interaction.user.id, filesize, safe_name),
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
logging.info(f"✅ {user_tag(interaction.user)} success {route_str}")
|
||||
|
||||
if not (1 <= filesize <= 9):
|
||||
published_msg = await interaction.channel.send(
|
||||
f"{interaction.user.mention} shared a [clip]({CLIPPY_BASE_URL}/{safe_name}.mp4) from [{route_str}](https://connect.comma.ai/{route_str})\n{title}"
|
||||
)
|
||||
else:
|
||||
published_msg = await interaction.channel.send(
|
||||
f"{interaction.user.mention} shared a clip from [{route_str}](https://connect.comma.ai/{route_str})\n{title}",
|
||||
file=discord.File(full_path)
|
||||
)
|
||||
await status_msg.edit(
|
||||
content="✅ Clip ready.", attachments=[], view=DeletePublishedView(published_msg, interaction.user.id, full_path),
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
clip_queue[:] = [j for j in clip_queue if j["route"] != safe_name]
|
||||
|
||||
finally:
|
||||
if user_id in user_cooldowns:
|
||||
user_cooldowns[user_id] = max(0, user_cooldowns[user_id] - 1)
|
||||
clip_queue[:] = [j for j in clip_queue if j["route"] != safe_name]
|
||||
|
||||
|
||||
@bot.tree.command(name="clippy-stats", description="View clippy stats")
|
||||
async def clippy_stats(interaction: discord.Interaction):
|
||||
|
||||
if interaction.guild_id not in ALLOWED_GUILD_IDS:
|
||||
logging.error(f"❌ This bot is not available in this server {interaction.guild_id}")
|
||||
await interaction.response.send_message("❌ This bot is not available in this server.", ephemeral=True)
|
||||
return
|
||||
|
||||
if not has_any_role(interaction.user, CLIPPY_STATS_ALLOWED_ROLES):
|
||||
logging.error(f"🚫 {user_tag(interaction.user)} not allowed to use /clippy-stats")
|
||||
await interaction.response.send_message("🚫 You don't have permission.", ephemeral=True)
|
||||
return
|
||||
|
||||
stat = f"Total: {stats['total']} | ✅ {stats['success']} | ❌ {stats['fail']}"
|
||||
queue = "\n".join(f"{j['status']} {j['user']}: {j['route']}" for j in clip_queue) or "No active jobs."
|
||||
tail = "\n".join(list(tail_buffer)[-5:][::-1]) or "[no log records yet]"
|
||||
|
||||
content = f"```{stat}``````{queue}``````{tail}"
|
||||
await interaction.response.send_message(content[:1997] + "```", ephemeral=True)
|
||||
logging.info(f"✅ {user_tag(interaction.user)} used /clippy-stats")
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
await bot.tree.sync()
|
||||
for guild in bot.guilds:
|
||||
logging.info(f"Connected to guild: {guild.name} ({guild.id})")
|
||||
await bot.change_presence(activity=discord.Game(name="your clips"))
|
||||
asyncio.create_task(queue_monitor())
|
||||
print(f"Logged in as {bot.user}")
|
||||
|
||||
threading.Thread(target=start_clip_server, daemon=True).start()
|
||||
bot.run(CLIPPY_TOKEN)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user