diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml
index 4f52e7fa2..d654f5ab4 100644
--- a/.github/workflows/sunnypilot-build-prebuilt.yaml
+++ b/.github/workflows/sunnypilot-build-prebuilt.yaml
@@ -9,12 +9,6 @@ env:
# Branch configurations
STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master' }} # vars are set on repo settings.
- DEV_C3_SOURCE_BRANCH: ${{ vars.DEV_C3_SOURCE_BRANCH || 'master-dev-c3-new' }} # vars are set on repo settings.
-
- # Target branch configurations
- STAGING_TARGET_BRANCH: ${{ vars.STAGING_TARGET_BRANCH || 'staging-c3-new' }} # vars are set on repo settings.
- DEV_TARGET_BRANCH: ${{ vars.DEV_TARGET_BRANCH || 'dev-c3-new' }} # vars are set on repo settings.
- RELEASE_TARGET_BRANCH: ${{ vars.RELEASE_TARGET_BRANCH || 'release-c3-new' }} # vars are set on repo settings.
# Runtime configuration
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
@@ -22,7 +16,7 @@ env:
on:
push:
branches: [ master, master-dev-c3-new ]
- tags: [ '*' ]
+ tags: [ 'release/*' ]
pull_request_target:
types: [ labeled ]
workflow_dispatch:
@@ -34,9 +28,72 @@ on:
default: false
jobs:
+ prepare_strategy:
+ runs-on: ubuntu-24.04
+ if: (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
+ outputs:
+ environment: ${{ steps.strategy.outputs.environment }}
+ new_branch: ${{ steps.strategy.outputs.new_branch }}
+ extra_version_identifier: ${{ steps.strategy.outputs.extra_version_identifier }}
+ version: ${{ steps.strategy.outputs.version }}
+ cancel_publish_in_progress: ${{ steps.strategy.outputs.cancel_publish_in_progress }}
+ publish_concurrency_group: ${{ steps.strategy.outputs.publish_concurrency_group }}
+ is_stable_branch: ${{ steps.strategy.outputs.is_stable_branch }}
+ build: ${{ steps.strategy.outputs.build }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Extract deploy strategy
+ id: strategy
+ run: |
+ echo '::group::Strategy Extraction'
+ BRANCH="${{ github.head_ref || github.ref_name }}"
+ echo "Current branch: $BRANCH"
+
+ STRATEGY_JSON='${{ vars.DEPLOY_STRATEGY }}'
+ CONFIG=$(echo "$STRATEGY_JSON" | jq -r --arg branch "$BRANCH" '
+ .configs[] | select(.branch == $branch)
+ ')
+
+ BUILD="$(date '+%Y.%m.%d')-${{ github.run_number }}"
+ if [[ -z "$CONFIG" || "$CONFIG" == "null" ]]; then
+ echo "No exact strategy match found. Falling back to feature/fork logic."
+ IS_FORK="${{ github.event.pull_request.head.repo.fork && 'true' || 'false' }}"
+ FORK_SUFFIX=$( [[ "$IS_FORK" == "true" ]] && echo "-fork" || echo "" )
+ NEW_BRANCH="${BRANCH}${FORK_SUFFIX}-prebuilt"
+
+ echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT
+ echo "version=$BUILD" >> $GITHUB_OUTPUT
+ echo "cancel_publish_in_progress=true" >> $GITHUB_OUTPUT
+ echo "publish_concurrency_group=publish-${BRANCH}" >> $GITHUB_OUTPUT
+ echo "environment=feature-branch" >> $GITHUB_OUTPUT
+ echo "extra_version_identifier=feature-branch" >> $GITHUB_OUTPUT
+ else
+ echo "Matched config: $CONFIG"
+ environment=$(echo "$CONFIG" | jq -r '.environment')
+ echo "environment=$environment" >> $GITHUB_OUTPUT
+ echo "new_branch=$(echo "$CONFIG" | jq -r '.target_branch')" >> $GITHUB_OUTPUT
+ cancel="$(echo "$CONFIG" | jq -r '.cancel_publish_in_progress')";
+ echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT
+ echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT
+
+ 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');
+ echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT
+ echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT
+ fi
+ echo "build=$BUILD" >> $GITHUB_OUTPUT
+ cat $GITHUB_OUTPUT
+
validate_tests:
runs-on: ubuntu-24.04
- if: ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
+ needs: [ prepare_strategy ]
+ if: ${{
+ ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) ||
+ (github.event_name == 'push' && needs.prepare_strategy.outputs.is_stable_branch == 'true') ||
+ contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
+ }}
steps:
- uses: actions/checkout@v4
- name: Wait for Tests
@@ -44,19 +101,26 @@ jobs:
with:
workflow: selfdrive_tests.yaml # The workflow file to monitor
github-token: ${{ secrets.GITHUB_TOKEN }}
+ should-wait-for-start: ${{ github.event_name == 'push' && 'true' || 'false' }}
build:
- needs: [ validate_tests ]
+ needs: [ validate_tests, prepare_strategy ]
concurrency:
group: build-${{ github.head_ref || github.ref_name }}
cancel-in-progress: false
runs-on: [self-hosted, tici]
outputs:
- new_branch: ${{ steps.set-env.outputs.new_branch }}
- version: ${{ steps.set-env.outputs.version }}
- extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }}
- commit_sha: ${{ steps.set-env.outputs.commit_sha }}
- if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
+ new_branch: ${{ needs.prepare_strategy.outputs.new_branch }}
+ version: ${{ needs.prepare_strategy.outputs.version }}
+ extra_version_identifier: ${{ needs.prepare_strategy.outputs.extra_version_identifier }}
+ commit_sha: ${{ github.sha }}
+ if: ${{
+ (always() && !cancelled() && !failure()) &&
+ needs.prepare_strategy.result == 'success' &&
+ (needs.validate_tests.result == 'success' || needs.validate_tests.result == 'skipped') &&
+ (!contains(github.event_name, 'pull_request') ||
+ (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
+ }}
steps:
- uses: actions/checkout@v4
with:
@@ -77,61 +141,12 @@ jobs:
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_C3_SOURCE_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}
- - name: Set Feature Branch Prebuilt Configuration
- id: set_feature_configuration
- if: (
- !(env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH) &&
- !(env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) &&
- !(startsWith(github.ref, 'refs/tags/'))
- )
- run: |
- echo "NEW_BRANCH=${{ env.SOURCE_BRANCH }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-prebuilt" >> $GITHUB_ENV
- echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV
-
- - name: Set dev-c3-new prebuilt Configuration
- id: set_dev_configuration
- if: (
- steps.set_feature_configuration.outcome == 'skipped' &&
- env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH
- )
- run: |
- echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV
- echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV
- echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV
-
- - name: Set staging-c3-new prebuilt Configuration
- id: set_staging_configuration
- if: (
- steps.set_feature_configuration.outcome == 'skipped' &&
- !contains(github.event_name, 'pull_request') &&
- steps.set_dev_configuration.outcome == 'skipped' &&
- (env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH)
- )
- run: |
- echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV
- echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV
- echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-staging" >> $GITHUB_ENV
-
- - name: Set release-c3-new prebuilt Configuration
- id: set_tag_configuration
- if: (
- steps.set_feature_configuration.outcome == 'skipped' &&
- !contains(github.event_name, 'pull_request') &&
- steps.set_staging_configuration.outcome == 'skipped' &&
- startsWith(github.ref, 'refs/tags/')
- )
- run: |
- echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV
- echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV
- echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-release" >> $GITHUB_ENV
-
- name: Set environment variables
id: set-env
run: |
- # Write to GITHUB_OUTPUT from environment variables
- echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT
- [[ ! -z "$EXTRA_VERSION_IDENTIFIER" ]] && echo "extra_version_identifier=$EXTRA_VERSION_IDENTIFIER" >> $GITHUB_OUTPUT
- [[ ! -z "$VERSION" ]] && echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "new_branch=${{ needs.prepare_strategy.outputs.new_branch }}" >> $GITHUB_OUTPUT
+ echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT
+ echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
# Set up common environment
@@ -226,15 +241,19 @@ jobs:
if: always()
run: |
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
-
+
+
publish:
concurrency:
- group: publish-${{ github.head_ref || github.ref_name }}
- cancel-in-progress: true
- if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
- needs: [ build ]
+ # We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name.
+ # This means that if multiple commits come in while we're publishing, they will be queued up and publish one after the other.
+ # Otherwise, if a job is waiting to be published due to environment wait time, it would be canceled by a new commit and restart the wait time.
+ group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }}
+ cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }}
+ if: ${{ (always() && !cancelled() && !failure()) && needs.build.result == 'success' && needs.prepare_strategy.result == 'success' && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
+ needs: [ build, prepare_strategy ]
runs-on: ubuntu-24.04
- environment: ${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }}
+ environment: ${{ needs.prepare_strategy.outputs.environment }}
steps:
- uses: actions/checkout@v4
@@ -266,7 +285,7 @@ jobs:
"${{ needs.build.outputs.new_branch }}" \
"${{ needs.build.outputs.version }}" \
"https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \
- "-${{ needs.build.outputs.extra_version_identifier }}"
+ "${{ needs.build.outputs.extra_version_identifier }}"
echo ""
echo "---- âšī¸ To update the list of branches that auto deploy prebuilts -----"
@@ -274,11 +293,18 @@ jobs:
echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES"
echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}"
echo "3. Update as needed (JSON array with no spaces)"
+
+ - name: Tag ${{ needs.prepare_strategy.outputs.environment }}
+ if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }}
+ run: |
+ TAG="${{ needs.prepare_strategy.outputs.environment }}/${{ needs.prepare_strategy.outputs.version }}/${{ needs.prepare_strategy.outputs.build }}"
+ git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.build.outputs.build }}."
+ git push -f origin ${TAG}
notify:
needs: [ build, publish ]
runs-on: ubuntu-24.04
- if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
+ if: ${{ (always() && !cancelled() && !failure()) && needs.publish.result == 'success' && !failure() && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
steps:
- uses: actions/checkout@v4
- name: Setup Alpine Linux environment
diff --git a/docs/CARS.md b/docs/CARS.md
index e10dc8ae7..dbc1dbcd7 100644
--- a/docs/CARS.md
+++ b/docs/CARS.md
@@ -4,12 +4,13 @@
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.
-# 326 Supported Cars
+# 334 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|
Hardware Needed
|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Acura|MDX 2025|All except Type S|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -72,12 +73,15 @@ A supported vehicle is one that just works when you install a comma device. All
|Genesis|GV80 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
-|Honda|Accord 2023|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Accord 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Accord Hybrid 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
-|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -85,7 +89,9 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Clarity 2018-21|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector + Honda Clarity Proxy Board
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|CR-V 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|CR-V Hybrid 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -96,6 +102,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
@@ -120,7 +127,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
-|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
+|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
@@ -298,6 +305,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
+|Toyota|Wildlander PHEV 2021|All|openpilot|19 mph|0 mph|[](##)|[](##)|Parts
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |||
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
||
diff --git a/launch_openpilot.sh b/launch_openpilot.sh
index d6e3424c3..d4841b601 100755
--- a/launch_openpilot.sh
+++ b/launch_openpilot.sh
@@ -1,3 +1,20 @@
#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+
+# On any failure, run the fallback launcher
+trap 'exec ./launch_chffrplus.sh' ERR
+C3_LAUNCH_SH="./sunnypilot/system/hardware/c3/launch_chffrplus.sh"
+
+MODEL="$(tr -d '\0' < "/sys/firmware/devicetree/base/model")"
+export MODEL
+
+if [ "$MODEL" = "comma tici" ]; then
+ # Force a failure if the launcher doesn't exist
+ [ -x "$C3_LAUNCH_SH" ] || false
+
+ # If it exists, run it
+ exec "$C3_LAUNCH_SH"
+fi
exec ./launch_chffrplus.sh
diff --git a/release/ci/publish.sh b/release/ci/publish.sh
index d72378293..4aecbacc4 100755
--- a/release/ci/publish.sh
+++ b/release/ci/publish.sh
@@ -51,26 +51,19 @@ git fetch origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --all
echo "[-] committing version $VERSION T=$SECONDS"
git add -f .
-git commit -a -m "sunnypilot v$VERSION release"
-git branch --set-upstream-to=origin/$DEV_BRANCH
# 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=$(cat $SOURCE_DIR/common/version.h | awk -F\" '{print $2}')
+SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/common/version.h)
-# Add built files to git
-git add -f .
-if [ "$EXTRA_VERSION_IDENTIFIER" = "-release" ] || [ "$EXTRA_VERSION_IDENTIFIER" = "-staging" ]; then
- export VERSION=${VERSION%"$EXTRA_VERSION_IDENTIFIER"}
- git commit --amend -m "sunnypilot v$VERSION"
-else
- git commit --amend -m "sunnypilot v$VERSION
- version: sunnypilot v$SP_VERSION release
- date: $DATETIME
- master commit: $GIT_HASH
- "
-fi
+# Commit with detailed message
+git commit -a -m "sunnypilot v$VERSION
+version: sunnypilot v$SP_VERSION (${EXTRA_VERSION_IDENTIFIER})
+date: $DATETIME
+master commit: $GIT_HASH
+"
+git branch --set-upstream-to=origin/$DEV_BRANCH
git branch -m $DEV_BRANCH
# Push!
diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py
index 37c9734f2..e756cfc6a 100644
--- a/selfdrive/controls/controlsd.py
+++ b/selfdrive/controls/controlsd.py
@@ -120,6 +120,8 @@ class Controls(ControlsExt, ModelStateBase):
self.LaC.update_live_torque_params(torque_params.latAccelFactorFiltered, torque_params.latAccelOffsetFiltered,
torque_params.frictionCoefficientFiltered)
+ self.LaC.extension.update_limits()
+
self.LaC.extension.update_model_v2(self.sm['modelV2'])
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py
index d04f9ed54..af0fe14bc 100644
--- a/selfdrive/controls/lib/latcontrol_torque.py
+++ b/selfdrive/controls/lib/latcontrol_torque.py
@@ -35,7 +35,7 @@ class LatControlTorque(LatControl):
self.update_limits()
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
- self.extension = LatControlTorqueExt(self, CP, CP_SP)
+ self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction):
self.torque_params.latAccelFactor = latAccelFactor
@@ -73,12 +73,6 @@ class LatControlTorque(LatControl):
ff = gravity_adjusted_lateral_accel
ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
- # Lateral acceleration torque controller extension updates
- # Overrides stock ff and pid_log.error
- ff, pid_log = self.extension.update(CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
- desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
- desired_curvature, actual_curvature)
-
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
output_lataccel = self.pid.update(pid_log.error,
feedforward=ff,
@@ -86,6 +80,12 @@ class LatControlTorque(LatControl):
freeze_integrator=freeze_integrator)
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
+ # Lateral acceleration torque controller extension updates
+ # Overrides pid_log.error and output_torque
+ pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
+ desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
+ desired_curvature, actual_curvature, steer_limited_by_safety, output_torque)
+
pid_log.active = True
pid_log.p = float(self.pid.p)
pid_log.i = float(self.pid.i)
diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript
index 810338aae..2f3c8ddd8 100644
--- a/selfdrive/ui/sunnypilot/SConscript
+++ b/selfdrive/ui/sunnypilot/SConscript
@@ -4,6 +4,7 @@ widgets_src = [
"sunnypilot/qt/widgets/controls.cc",
"sunnypilot/qt/widgets/drive_stats.cc",
"sunnypilot/qt/widgets/expandable_row.cc",
+ "sunnypilot/qt/widgets/external_storage.cc",
"sunnypilot/qt/widgets/prime.cc",
"sunnypilot/qt/widgets/scrollview.cc",
"sunnypilot/qt/network/networking.cc",
diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
index 58193f9fe..a4a6bf481 100644
--- a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
+++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc
@@ -5,9 +5,14 @@
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h"
+#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h"
DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(parent) {
+ #ifndef __APPLE__
+ addItem(new ExternalStorageControl());
+ #endif
+
// Advanced Controls Toggle
showAdvancedControls = new ParamControlSP("ShowAdvancedControls", tr("Show Advanced Controls"), tr("Toggle visibility of advanced sunnypilot controls.\nThis only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state."), "");
addItem(showAdvancedControls);
diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc
index 3d4e07096..96d945ab5 100644
--- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc
+++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc
@@ -34,6 +34,27 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
vlayout->setContentsMargins(50, 20, 50, 20);
auto *list = new ListWidget(this, false);
+
+ QVBoxLayout *titleLayout = new QVBoxLayout;
+ QLabel *title = new QLabel(tr("đ sunnylink đ"));
+ title->setStyleSheet("font-size: 90px; font-weight: 500; font-family: 'Noto Color Emoji';");
+ titleLayout->addWidget(title, 0, Qt::AlignCenter);
+
+ QLabel *sunnylinkDesc = new QLabel("
"+
+ tr("For secure backup, restore, and remote configuration")+ "
");
+
+ QLabel *sponsorMsg = new QLabel(""+
+ tr("Sponsorship isn't required for basic backup/restore") + "
" +
+ tr("Click the sponsor button for more details")+ "
");
+
+ sunnylinkDesc->setStyleSheet("font-size: 40px; font-weight: 100; font-family: 'Noto';");
+ sponsorMsg->setStyleSheet("font-size: 35px; font-weight: 100; font-family: 'Noto';");
+
+ titleLayout->addWidget(sunnylinkDesc, 0, Qt::AlignCenter);
+ titleLayout->addWidget(sponsorMsg, 0, Qt::AlignCenter);
+
+ list->addItem(titleLayout);
+
QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that.");
sunnylinkEnabledBtn = new ParamControl(
"SunnylinkEnabled",
diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc
new file mode 100644
index 000000000..f95118751
--- /dev/null
+++ b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+ *
+ * This file is part of sunnypilot and is licensed under the MIT License.
+ * See the LICENSE.md file in the root directory for more details.
+ */
+
+#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "common/params.h"
+#include "selfdrive/ui/qt/api.h"
+#include "selfdrive/ui/qt/widgets/input.h"
+#include "selfdrive/ui/sunnypilot/ui.h"
+
+ExternalStorageControl::ExternalStorageControl() :
+ ButtonControl(tr("External Storage"), "", tr("Extend your comma device's storage by inserting a USB drive into the aux port.")) {
+
+ QObject::connect(this, &ButtonControl::clicked, [=]() {
+ if (text() == tr("CHECK") || text() == tr("MOUNT")) {
+ mountStorage();
+ } else if (text() == tr("UNMOUNT")) {
+ unmountStorage();
+ } else if (text() == tr("FORMAT")) {
+ if (ConfirmationDialog::confirm(tr("Are you sure you want to format this drive? This will erase all data."), tr("Format"), this)) {
+ formatStorage();
+ }
+ }
+ });
+
+ QObject::connect(uiState(), &UIState::offroadTransition, this, &ExternalStorageControl::updateState);
+ updateState(!uiState()->scene.started);
+
+ refresh();
+}
+
+void ExternalStorageControl::updateState(bool offroad) {
+ setEnabled(offroad);
+}
+
+void ExternalStorageControl::debouncedRefresh() {
+ if (refreshPending) return;
+ refreshPending = true;
+
+ QTimer::singleShot(250, this, [=]() {
+ refreshPending = false;
+ refresh();
+ });
+}
+
+void ExternalStorageControl::refresh() {
+ QtConcurrent::run([=]() {
+ auto run = [](const QString &cmd) {
+ QProcess p;
+ p.start("sh", QStringList() << "-c" << cmd);
+ p.waitForFinished();
+ return p.exitCode() == 0;
+ };
+
+ bool isMounted = run("findmnt -n /mnt/external_realdata");
+ bool hasDrive = run("lsblk -f /dev/sdg");
+ bool hasFs = run("lsblk -f /dev/sdg1 | grep -q ext4");
+ bool hasLabel = run("sudo blkid /dev/sdg1 | grep -q 'LABEL=\"openpilot\"'");
+
+ QString info;
+ if (isMounted && hasLabel) {
+ QProcess df;
+ df.start("sh", QStringList() << "-c" << "df -h /mnt/external_realdata | awk 'NR==2 {print $3 \"/\" $2}'");
+ df.waitForFinished();
+ info = df.readAllStandardOutput().trimmed();
+ }
+
+ QMetaObject::invokeMethod(this, [=]() {
+ if (formatting) {
+ setValue(tr("formatting"));
+ setText(tr("FORMAT"));
+ setEnabled(false);
+ } else {
+ if (!hasDrive) {
+ setValue(tr("insert drive"));
+ setText(tr("CHECK"));
+ } else if (!hasFs || !hasLabel) {
+ setValue(tr("needs format"));
+ setText(tr("FORMAT"));
+ } else if (isMounted) {
+ setValue(info);
+ setText(tr("UNMOUNT"));
+ } else {
+ setValue("drive detected");
+ setText(tr("MOUNT"));
+ }
+ updateState(!uiState()->scene.started);
+ }
+ }, Qt::QueuedConnection);
+ });
+}
+
+void ExternalStorageControl::mountStorage() {
+ setValue(tr("mounting"));
+ setEnabled(false);
+
+ QtConcurrent::run([=]() {
+ QProcess process;
+ process.start("sh", QStringList() << "-c" <<
+ "sudo mount -o remount,rw / && "
+ "sudo mkdir -p /mnt/external_realdata && "
+ "grep -q '/dev/sdg1 /mnt/external_realdata' /etc/fstab || "
+ "echo '/dev/sdg1 /mnt/external_realdata ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab && "
+ "sudo systemctl daemon-reexec && "
+ "sudo mount /mnt/external_realdata && "
+ "sudo chown -R comma:comma /mnt/external_realdata && "
+ "sudo chmod -R 775 /mnt/external_realdata && "
+ "sudo mount -o remount,ro /");
+ process.waitForFinished();
+
+ QMetaObject::invokeMethod(this, [=]() {
+ debouncedRefresh();
+ }, Qt::QueuedConnection);
+ });
+}
+
+void ExternalStorageControl::unmountStorage() {
+ setValue(tr("unmounting"));
+ setEnabled(false);
+
+ QtConcurrent::run([=]() {
+ QProcess process;
+ process.start("sh", QStringList() << "-c" << "sudo umount /mnt/external_realdata");
+ process.waitForFinished();
+
+ QMetaObject::invokeMethod(this, [=]() {
+ debouncedRefresh();
+ }, Qt::QueuedConnection);
+ });
+}
+
+void ExternalStorageControl::formatStorage() {
+ unmountStorage();
+ formatting = true;
+ setValue(tr("formatting"));
+ setEnabled(false);
+
+ QProcess *process = new QProcess(this);
+ connect(process, static_cast(&QProcess::finished),
+ this, [=](int exitCode, QProcess::ExitStatus status) {
+ process->deleteLater();
+ formatting = false;
+ if (exitCode == 0 && status == QProcess::NormalExit) {
+ mountStorage();
+ } else {
+ setValue(tr("needs format"));
+ updateState(!uiState()->scene.started);
+ }
+ });
+ process->start("sh", QStringList() << "-c" <<
+ "sudo wipefs -a /dev/sdg && "
+ "sudo parted -s /dev/sdg mklabel gpt mkpart primary ext4 0% 100% && "
+ "sudo mkfs.ext4 -F -L openpilot /dev/sdg1"
+ );
+}
+
+void ExternalStorageControl::showEvent(QShowEvent *event) {
+ ButtonControl::showEvent(event);
+ QTimer::singleShot(100, this, &ExternalStorageControl::debouncedRefresh);
+}
diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h
new file mode 100644
index 000000000..d26eefd18
--- /dev/null
+++ b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+ *
+ * This file is part of sunnypilot and is licensed under the MIT License.
+ * See the LICENSE.md file in the root directory for more details.
+ */
+
+#pragma once
+
+#include "system/hardware/hw.h"
+#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
+#define ButtonControl ButtonControlSP
+
+class ExternalStorageControl : public ButtonControl {
+ Q_OBJECT
+
+public:
+ ExternalStorageControl();
+
+protected:
+ void showEvent(QShowEvent *event) override;
+
+private:
+ Params params;
+
+ bool refreshPending = false;
+ bool formatting = false;
+ void updateState(bool offroad);
+ void refresh();
+ void debouncedRefresh();
+ void mountStorage();
+ void unmountStorage();
+ void formatStorage();
+};
diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts
index f72ea8ee8..ef7110e72 100644
--- a/selfdrive/ui/translations/main_ar.ts
+++ b/selfdrive/ui/translations/main_ar.ts
@@ -2103,6 +2103,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts
index 193e2fd89..dde924146 100644
--- a/selfdrive/ui/translations/main_de.ts
+++ b/selfdrive/ui/translations/main_de.ts
@@ -2085,6 +2085,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts
index 1d1723cde..3b53e7f94 100644
--- a/selfdrive/ui/translations/main_es.ts
+++ b/selfdrive/ui/translations/main_es.ts
@@ -2087,6 +2087,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts
index 959321bb4..45705defd 100644
--- a/selfdrive/ui/translations/main_fr.ts
+++ b/selfdrive/ui/translations/main_fr.ts
@@ -2083,6 +2083,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts
index 28b5b00e5..3faa18395 100644
--- a/selfdrive/ui/translations/main_ja.ts
+++ b/selfdrive/ui/translations/main_ja.ts
@@ -2082,6 +2082,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts
index d69fda88b..756cad604 100644
--- a/selfdrive/ui/translations/main_ko.ts
+++ b/selfdrive/ui/translations/main_ko.ts
@@ -2096,6 +2096,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts
index c5171b19f..0a886d435 100644
--- a/selfdrive/ui/translations/main_pt-BR.ts
+++ b/selfdrive/ui/translations/main_pt-BR.ts
@@ -2087,6 +2087,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts
index 3bd2d84a9..7b89f2a69 100644
--- a/selfdrive/ui/translations/main_th.ts
+++ b/selfdrive/ui/translations/main_th.ts
@@ -2078,6 +2078,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts
index db9c15580..0598d0836 100644
--- a/selfdrive/ui/translations/main_tr.ts
+++ b/selfdrive/ui/translations/main_tr.ts
@@ -2077,6 +2077,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts
index 231636089..1f4db2d6d 100644
--- a/selfdrive/ui/translations/main_zh-CHS.ts
+++ b/selfdrive/ui/translations/main_zh-CHS.ts
@@ -2082,6 +2082,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts
index 16bb393de..1a8784846 100644
--- a/selfdrive/ui/translations/main_zh-CHT.ts
+++ b/selfdrive/ui/translations/main_zh-CHT.ts
@@ -2082,6 +2082,22 @@ Warning: You are on a metered connection!
[Don't use] Enable sunnylink uploader
+
+ đ sunnylink đ
+
+
+
+ For secure backup, restore, and remote configuration
+
+
+
+ Sponsorship isn't required for basic backup/restore
+
+
+
+ Click the sponsor button for more details
+
+
SunnylinkSponsorPopup
diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py
index fe54f46ca..6e134276e 100644
--- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py
+++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py
@@ -9,23 +9,28 @@ from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.nnlc import NeuralNetworkL
class LatControlTorqueExt(NeuralNetworkLateralControl):
- def __init__(self, lac_torque, CP, CP_SP):
- super().__init__(lac_torque, CP, CP_SP)
+ def __init__(self, lac_torque, CP, CP_SP, CI):
+ super().__init__(lac_torque, CP, CP_SP, CI)
- def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
+ def update(self, CS, VM, pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
- desired_curvature, actual_curvature):
+ desired_curvature, actual_curvature, steer_limited_by_safety, output_torque):
self._ff = ff
+ self._pid = pid
self._pid_log = pid_log
self._setpoint = setpoint
self._measurement = measurement
+ self._roll_compensation = roll_compensation
self._lateral_accel_deadzone = lateral_accel_deadzone
self._desired_lateral_accel = desired_lateral_accel
self._actual_lateral_accel = actual_lateral_accel
self._desired_curvature = desired_curvature
self._actual_curvature = actual_curvature
+ self._gravity_adjusted_lateral_accel = gravity_adjusted_lateral_accel
+ self._steer_limited_by_safety = steer_limited_by_safety
+ self._output_torque = output_torque
self.update_calculations(CS, VM, desired_lateral_accel)
self.update_neural_network_feedforward(CS, params, calibrated_pose)
- return self._ff, self._pid_log
+ return self._pid_log, self._output_torque
diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py
index 644f28573..1965d50b5 100644
--- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py
+++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
import math
import numpy as np
+from openpilot.common.pid import PIDController
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
from openpilot.selfdrive.modeld.constants import ModelConstants
@@ -43,9 +44,10 @@ def get_lookahead_value(future_vals, current_val):
class LatControlTorqueExtBase:
- def __init__(self, lac_torque, CP, CP_SP):
+ def __init__(self, lac_torque, CP, CP_SP, CI):
self.model_v2 = None
self.model_valid = False
+ self.lac_torque = lac_torque
self.torque_params = lac_torque.torque_params
self.actual_lateral_jerk: float = 0.0
@@ -53,17 +55,22 @@ class LatControlTorqueExtBase:
self.lateral_jerk_measurement: float = 0.0
self.lookahead_lateral_jerk: float = 0.0
- self.torque_from_lateral_accel = lac_torque.torque_from_lateral_accel
+ self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space()
self._ff = 0.0
+ self._pid = PIDController(0.0, 0.0, k_f=0.0)
self._pid_log = None
self._setpoint = 0.0
self._measurement = 0.0
+ self._roll_compensation = 0.0
self._lateral_accel_deadzone = 0.0
self._desired_lateral_accel = 0.0
self._actual_lateral_accel = 0.0
self._desired_curvature = 0.0
self._actual_curvature = 0.0
+ self._gravity_adjusted_lateral_accel = 0.0
+ self._steer_limited_by_safety = False
+ self._output_torque = 0.0
# twilsonco's Lateral Neural Network Feedforward
# Instantaneous lateral jerk changes very rapidly, making it not useful on its own,
diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py
index 77d46ace3..1738a11e4 100644
--- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py
+++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py
@@ -9,6 +9,8 @@ import math
import numpy as np
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
+from opendbc.sunnypilot.car.interfaces import LatControlInputs
+from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_torque_space
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.selfdrive.modeld.constants import ModelConstants
@@ -30,8 +32,8 @@ def roll_pitch_adjust(roll, pitch):
class NeuralNetworkLateralControl(LatControlTorqueExtBase):
- def __init__(self, lac_torque, CP, CP_SP):
- super().__init__(lac_torque, CP, CP_SP)
+ def __init__(self, lac_torque, CP, CP_SP, CI):
+ super().__init__(lac_torque, CP, CP_SP, CI)
self.params = Params()
self.enabled = self.params.get_bool("NeuralNetworkLateralControl")
self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH
@@ -57,14 +59,44 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
self.error_deque = deque(maxlen=history_check_frames[0])
self.past_future_len = len(self.past_times) + len(self.nn_future_times)
+ @property
+ def _nnlc_enabled(self):
+ return self.enabled and self.model_valid and self.has_nn_model
+
+ def update_limits(self):
+ if not self._nnlc_enabled:
+ return
+
+ self._pid.set_limits(self.lac_torque.steer_max, -self.lac_torque.steer_max)
+
def update_lateral_lag(self, lag):
super().update_lateral_lag(lag)
self.nn_future_times = [t + self.desired_lat_jerk_time for t in self.future_times]
+ def update_feedforward_torque_space(self, CS):
+ torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._setpoint, self._roll_compensation, CS.vEgo, CS.aEgo),
+ self.torque_params, gravity_adjusted=False)
+ torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo),
+ self.torque_params, gravity_adjusted=False)
+ self._pid_log.error = float(torque_from_setpoint - torque_from_measurement)
+ self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation,
+ CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True)
+ self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
+ FRICTION_THRESHOLD, self.torque_params)
+
+ def update_output_torque(self, CS):
+ freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
+ self._output_torque = self._pid.update(self._pid_log.error,
+ feedforward=self._ff,
+ speed=CS.vEgo,
+ freeze_integrator=freeze_integrator)
+
def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None:
- if not self.enabled or not self.model_valid or not self.has_nn_model:
+ if not self._nnlc_enabled:
return
+ self.update_feedforward_torque_space(CS)
+
low_speed_factor = float(np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)) ** 2
self._setpoint = self._desired_lateral_accel + low_speed_factor * self._desired_curvature
self._measurement = self._actual_lateral_accel + low_speed_factor * self._actual_curvature
@@ -128,3 +160,5 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
# apply friction override for cars with low NN friction response
if self.model.friction_override:
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
+
+ self.update_output_torque(CS)
diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py
index 01ddec68a..009e3d96a 100644
--- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py
+++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py
@@ -3,6 +3,7 @@ from parameterized import parameterized
from cereal import car, log, messaging
from opendbc.car.car_helpers import interfaces
+from opendbc.car.gm.values import CAR as GM
from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.hyundai.values import CAR as HYUNDAI
from opendbc.car.toyota.values import CAR as TOYOTA
@@ -41,7 +42,7 @@ def generate_modelV2():
class TestNeuralNetworkLateralControl:
- @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN])
+ @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN, GM.CHEVROLET_BOLT_EUV])
def test_saturation(self, car_name):
params = Params()
params.put_bool("NeuralNetworkLateralControl", True)
@@ -57,6 +58,7 @@ class TestNeuralNetworkLateralControl:
VM = VehicleModel(CP)
controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI)
+ torque_params = CP.lateralTuning.torque
CS = car.CarState.new_message()
CS.vEgo = 30
@@ -77,17 +79,23 @@ class TestNeuralNetworkLateralControl:
for _ in range(1000):
controller.extension.update_model_v2(model_v2)
controller.extension.update_lateral_lag(test_lag)
+ controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction)
+ controller.extension.update_limits()
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True)
assert lac_log.saturated
for _ in range(1000):
controller.extension.update_model_v2(model_v2)
controller.extension.update_lateral_lag(test_lag)
+ controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction)
+ controller.extension.update_limits()
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False)
assert not lac_log.saturated
for _ in range(1000):
controller.extension.update_model_v2(model_v2)
controller.extension.update_lateral_lag(test_lag)
+ controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction)
+ controller.extension.update_limits()
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False)
assert lac_log.saturated
diff --git a/sunnypilot/system/hardware/c3/README.md b/sunnypilot/system/hardware/c3/README.md
new file mode 100644
index 000000000..f74a21019
--- /dev/null
+++ b/sunnypilot/system/hardware/c3/README.md
@@ -0,0 +1,3 @@
+# C3 specific hardware code
+
+`c3` is known as `tici` and comma three by comma. Not to confuse it with `c3x` which is known as `tizi`.
\ No newline at end of file
diff --git a/sunnypilot/system/hardware/c3/agnos.json b/sunnypilot/system/hardware/c3/agnos.json
new file mode 100644
index 000000000..941a4956b
--- /dev/null
+++ b/sunnypilot/system/hardware/c3/agnos.json
@@ -0,0 +1,84 @@
+[
+ {
+ "name": "xbl",
+ "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz",
+ "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
+ "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
+ "size": 3282256,
+ "sparse": false,
+ "full_check": true,
+ "has_ab": true,
+ "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89"
+ },
+ {
+ "name": "xbl_config",
+ "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz",
+ "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
+ "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
+ "size": 98124,
+ "sparse": false,
+ "full_check": true,
+ "has_ab": true,
+ "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f"
+ },
+ {
+ "name": "abl",
+ "url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz",
+ "hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
+ "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
+ "size": 274432,
+ "sparse": false,
+ "full_check": true,
+ "has_ab": true,
+ "ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6"
+ },
+ {
+ "name": "aop",
+ "url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz",
+ "hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
+ "hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
+ "size": 184364,
+ "sparse": false,
+ "full_check": true,
+ "has_ab": true,
+ "ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180"
+ },
+ {
+ "name": "devcfg",
+ "url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz",
+ "hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
+ "hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
+ "size": 40336,
+ "sparse": false,
+ "full_check": true,
+ "has_ab": true,
+ "ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f"
+ },
+ {
+ "name": "boot",
+ "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
+ "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
+ "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
+ "size": 18515968,
+ "sparse": false,
+ "full_check": true,
+ "has_ab": true,
+ "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
+ },
+ {
+ "name": "system",
+ "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
+ "hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
+ "hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
+ "size": 5368709120,
+ "sparse": true,
+ "full_check": false,
+ "has_ab": true,
+ "ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
+ "alt": {
+ "hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
+ "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
+ "size": 5368709120
+ }
+ }
+]
\ No newline at end of file
diff --git a/sunnypilot/system/hardware/c3/launch_chffrplus.sh b/sunnypilot/system/hardware/c3/launch_chffrplus.sh
new file mode 100755
index 000000000..45cc95053
--- /dev/null
+++ b/sunnypilot/system/hardware/c3/launch_chffrplus.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+
+SP_C3_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+DIR="$( cd "$SP_C3_DIR/../../../.." >/dev/null 2>&1 && pwd )"
+
+source "$SP_C3_DIR/launch_env.sh"
+
+function agnos_init {
+ # TODO: move this to agnos
+ sudo rm -f /data/etc/NetworkManager/system-connections/*.nmmeta
+
+ # set success flag for current boot slot
+ sudo abctl --set_success
+
+ # TODO: do this without udev in AGNOS
+ # udev does this, but sometimes we startup faster
+ sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
+ sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
+
+
+ if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
+ AGNOS_PY="$DIR/system/hardware/tici/agnos.py"
+ MANIFEST="$SP_C3_DIR/agnos.json"
+ if $AGNOS_PY --verify $MANIFEST; then
+ sudo reboot
+ fi
+ $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST
+ fi
+}
+
+function launch {
+ # Remove orphaned git lock if it exists on boot
+ [ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock
+
+ # Check to see if there's a valid overlay-based update available. Conditions
+ # are as follows:
+ #
+ # 1. The DIR init file has to exist, with a newer modtime than anything in
+ # the DIR Git repo. This checks for local development work or the user
+ # switching branches/forks, which should not be overwritten.
+ # 2. The FINALIZED consistent file has to exist, indicating there's an update
+ # that completed successfully and synced to disk.
+
+ if [ -f "${DIR}/.overlay_init" ]; then
+ find ${DIR}/.git -newer ${DIR}/.overlay_init | grep -q '.' 2> /dev/null
+ if [ $? -eq 0 ]; then
+ echo "${DIR} has been modified, skipping overlay update installation"
+ else
+ if [ -f "${STAGING_ROOT}/finalized/.overlay_consistent" ]; then
+ if [ ! -d /data/safe_staging/old_openpilot ]; then
+ echo "Valid overlay update found, installing"
+ LAUNCHER_LOCATION="${BASH_SOURCE[0]}"
+
+ mv $DIR /data/safe_staging/old_openpilot
+ mv "${STAGING_ROOT}/finalized" $DIR
+ cd $DIR
+
+ echo "Restarting launch script ${LAUNCHER_LOCATION}"
+ unset AGNOS_VERSION
+ exec "${LAUNCHER_LOCATION}"
+ else
+ echo "openpilot backup found, not updating"
+ # TODO: restore backup? This means the updater didn't start after swapping
+ fi
+ fi
+ fi
+ fi
+
+ # handle pythonpath
+ ln -sfn $(pwd) /data/pythonpath
+ export PYTHONPATH="$PWD"
+
+ # hardware specific init
+ if [ -f /AGNOS ]; then
+ agnos_init
+ fi
+
+ # write tmux scrollback to a file
+ tmux capture-pane -pq -S-1000 > /tmp/launch_log
+
+ # start manager
+ cd $DIR/system/manager
+ if [ ! -f $DIR/prebuilt ]; then
+ ./build.py
+ fi
+ ./manager.py
+
+ # if broken, keep on screen error
+ while true; do sleep 1; done
+}
+
+launch
diff --git a/sunnypilot/system/hardware/c3/launch_env.sh b/sunnypilot/system/hardware/c3/launch_env.sh
new file mode 100755
index 000000000..593aacc07
--- /dev/null
+++ b/sunnypilot/system/hardware/c3/launch_env.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+export OMP_NUM_THREADS=1
+export MKL_NUM_THREADS=1
+export NUMEXPR_NUM_THREADS=1
+export OPENBLAS_NUM_THREADS=1
+export VECLIB_MAXIMUM_THREADS=1
+
+if [ -z "$AGNOS_VERSION" ]; then
+ export AGNOS_VERSION="12.8"
+fi
diff --git a/system/athena/athenad.py b/system/athena/athenad.py
index f97b8e55b..42c9cf8a1 100755
--- a/system/athena/athenad.py
+++ b/system/athena/athenad.py
@@ -381,20 +381,22 @@ def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str = N
return {"success": 1}
-def scan_dir(path: str, prefix: str) -> list[str]:
+def scan_dir(path: str, prefix: str, base: str | None = None) -> list[str]:
+ if base is None:
+ base = path
files = []
# only walk directories that match the prefix
# (glob and friends traverse entire dir tree)
with os.scandir(path) as i:
for e in i:
- rel_path = os.path.relpath(e.path, Paths.log_root())
+ rel_path = os.path.relpath(e.path, base)
if e.is_dir(follow_symlinks=False):
# add trailing slash
rel_path = os.path.join(rel_path, '')
# if prefix is a partial dir name, current dir will start with prefix
# if prefix is a partial file name, prefix with start with dir name
if rel_path.startswith(prefix) or prefix.startswith(rel_path):
- files.extend(scan_dir(e.path, prefix))
+ files.extend(scan_dir(e.path, prefix, base))
else:
if rel_path.startswith(prefix):
files.append(rel_path)
@@ -402,7 +404,12 @@ def scan_dir(path: str, prefix: str) -> list[str]:
@dispatcher.add_method
def listDataDirectory(prefix='') -> list[str]:
- return scan_dir(Paths.log_root(), prefix)
+ internal_files = scan_dir(Paths.log_root(), prefix, Paths.log_root())
+ try:
+ external_files = scan_dir(Paths.log_root_external(), prefix, Paths.log_root_external())
+ except FileNotFoundError:
+ external_files = []
+ return sorted(set(internal_files + external_files))
@dispatcher.add_method
@@ -427,8 +434,13 @@ def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlRespo
failed.append(file.fn)
continue
- path = os.path.join(Paths.log_root(), file.fn)
- if not os.path.exists(path) and not os.path.exists(strip_zst_extension(path)):
+ path_internal = os.path.join(Paths.log_root(), file.fn)
+ path_external = os.path.join(Paths.log_root_external(), file.fn)
+ if os.path.exists(path_internal) or os.path.exists(strip_zst_extension(path_internal)):
+ path = path_internal
+ elif os.path.exists(path_external) or os.path.exists(strip_zst_extension(path_external)):
+ path = path_external
+ else:
failed.append(file.fn)
continue
diff --git a/system/hardware/hw.py b/system/hardware/hw.py
index 5e40fff13..d24857e8b 100644
--- a/system/hardware/hw.py
+++ b/system/hardware/hw.py
@@ -20,6 +20,10 @@ class Paths:
else:
return '/data/media/0/realdata/'
+ @staticmethod
+ def log_root_external() -> str:
+ return '/mnt/external_realdata/'
+
@staticmethod
def swaglog_root() -> str:
if PC:
diff --git a/system/loggerd/config.py b/system/loggerd/config.py
index e1c47c768..d9befb561 100644
--- a/system/loggerd/config.py
+++ b/system/loggerd/config.py
@@ -9,21 +9,26 @@ STATS_DIR_FILE_LIMIT = 10000
STATS_SOCKET = "ipc:///tmp/stats"
STATS_FLUSH_TIME_S = 60
-def get_available_percent(default: float) -> float:
+PATH_DICT = {
+ "internal": Paths.log_root(),
+ "external": Paths.log_root_external()
+}
+
+def get_available_percent(default: float, path_type="internal") -> float:
try:
- statvfs = os.statvfs(Paths.log_root())
+ statvfs = os.statvfs(PATH_DICT[path_type])
available_percent = 100.0 * statvfs.f_bavail / statvfs.f_blocks
- except OSError:
+ except (OSError, KeyError):
available_percent = default
return available_percent
-def get_available_bytes(default: int) -> int:
+def get_available_bytes(default: int, path_type="internal") -> int:
try:
- statvfs = os.statvfs(Paths.log_root())
+ statvfs = os.statvfs(PATH_DICT[path_type])
available_bytes = statvfs.f_bavail * statvfs.f_frsize
- except OSError:
+ except (OSError, KeyError):
available_bytes = default
return available_bytes
diff --git a/system/loggerd/deleter.py b/system/loggerd/deleter.py
index eb8fd35f2..058f5c301 100755
--- a/system/loggerd/deleter.py
+++ b/system/loggerd/deleter.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import os
+import time
import shutil
import threading
+from pathlib import Path
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
from openpilot.system.loggerd.config import get_available_bytes, get_available_percent
@@ -61,6 +63,41 @@ def deleter_thread(exit_event: threading.Event):
if any(name.endswith(".lock") for name in os.listdir(delete_path)):
continue
+ if Path(Paths.log_root_external()).is_mount():
+ out_of_bytes_external = get_available_bytes(default=MIN_BYTES + 1, path_type="external") < MIN_BYTES
+ out_of_percent_external = get_available_percent(default=MIN_PERCENT + 1, path_type="external") < MIN_PERCENT
+
+ if out_of_percent_external or out_of_bytes_external:
+ dirs_external = listdir_by_creation(Paths.log_root_external())
+
+ # remove the earliest external directory we can
+ for delete_dir_external in sorted(dirs_external):
+ delete_path_external = os.path.join(Paths.log_root_external(), delete_dir_external)
+ try:
+ cloudlog.warning(f"deleting {delete_path_external}")
+ shutil.rmtree(delete_path_external)
+ break
+ except OSError:
+ cloudlog.exception(f"issue deleting {delete_path_external}")
+
+ # move directory from internal to external
+ path_external = os.path.join(Paths.log_root_external(), delete_dir)
+ try:
+ cloudlog.warning(f"moving {delete_path} to {path_external}")
+ start = time.monotonic()
+ shutil.move(delete_path, path_external)
+ cloudlog.warning(f"moved {delete_path} to {path_external} in {time.monotonic() - start:.2f}s")
+ break
+ except Exception:
+ cloudlog.error(f"issue moving {delete_path} to {path_external}")
+ try:
+ cloudlog.warning(f"deleting {delete_path}")
+ shutil.rmtree(delete_path)
+ break
+ except OSError:
+ cloudlog.exception(f"issue deleting {delete_path}")
+ continue
+
try:
cloudlog.info(f"deleting {delete_path}")
shutil.rmtree(delete_path)
diff --git a/uv.lock b/uv.lock
index 49ae16099..7010cdfb8 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4832,11 +4832,11 @@ wheels = [
[[package]]
name = "typing-extensions"
-version = "4.14.1"
+version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]