Compare commits

..

73 Commits

Author SHA1 Message Date
James Vecellio
9e131b4a69 TCPv3 plus gWM9 Model 2025-11-04 18:39:26 -08:00
James Vecellio
f402487f9e This one is actually cool
CgWM + ST
2025-11-04 17:05:46 -08:00
James Vecellio
2c922afa12 tcpv3 and uhhh i forgot the other model
model_checkpoint: merged with 0.875w from (model1: 'fd9a6816-8758-466b-bbde-3c1413b98f0a/400') and (model2: '0e620593-e85f-40c2-9adf-1e945651ed13/400')
2025-11-04 16:01:15 -08:00
discountchubbs
342abcd45a tcpv3 + gwmv9 2025-11-04 15:44:20 -08:00
discountchubbs
6e8586e566 NNMv2 + gWM9 2025-11-04 06:52:47 -08:00
Matt Purnell
071147baaf docs: Update README installation branches and discord links (#1453)
* Use sunnypilot CARS.md, update number of supported cars, add comma

* Update device reference

* Update discord links to forum links

* Update references to -c3-new branches and release

* Update broken link to branches table

* Update README.md

---------

Co-authored-by: DevTekVE <devtekve@gmail.com>
2025-11-03 06:52:17 +01:00
DevTekVE
18af4d6ad6 ui: Fix spacing in sunnylink panel (#1450)
Fix spacing
2025-11-02 20:26:17 +01:00
DevTekVE
b81d5bca3c ui: update discord references and add forum widget (#1440)
* sunnylink: introduce community popup with QR code embedding

- Added `SunnylinkCommunityPopup` widget to promote the sunnypilot Community Forum.
- Integrated a QR code generator and display for quick access.
- Updated `WiFiPromptWidget` to include a "Learn More" button triggering the community popup.

* sunnylink: adjust community popup styling for better layout

- Reduced font size of description text slightly for consistency.
- Decreased QR code dimensions to improve visual balance.

* Making more space out of thin air

* sunnylink: update community references to use forum links

- Replaced Discord links with Community Forum URLs for better alignment.
- Improved clarity in sponsorship instructions.
2025-11-02 06:50:41 +01:00
Amy Jeanes
682d738ffa Tesla: Coop Steering (#1283)
* Tesla: Coop Steering

* bump

* bump

* sync with opendbc/master

* resolve comment

* add oscillation warning and add confirmation

* styling desc

* beta

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-01 22:47:30 -04:00
DevTekVE
f60c2b6a83 sunnylink: update uploader button logic to support novice tier and above (#1438)
* sunnylink: update uploader button logic to support novice tier and above

- Adjusted the enable condition to include SponsorTier::Novice and above.

* sunnylink: improve uploader button visibility and accessibility logic

- Made uploader button conditionally visible based on user tier and settings.
- Clarified button label to specify testing purposes only.
2025-11-01 12:14:57 +01:00
DevTekVE
f833819143 ci: update trigger for prebuilt (#1439)
Updated workflow `if` conditions to use `vars.PREBUILT_PR_LABEL`.
2025-10-31 17:54:39 +01:00
THERoenPR
707e2aedae controlsd: add CP_SP to get_pid_accel_limits (#1410)
* Add CP_SP to get_pid_accel_limits() call in controlsd

Match input parameters of CP_SP commit

* bump

* bump

---------

Co-authored-by: roenthomas <43324106+roenthomas@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-28 22:13:39 -04:00
DevTekVE
55147d8a55 ci: use environment variable for PR label in query (#1436)
* ci: use environment variable for PR label in query

- Replaced static `PR_LABEL` references with `${{ env.PR_LABEL }}` for consistency.
- Ensures flexibility and reduces hardcoded values in the workflow.

* does this work better?

* fuck this

* aight
2025-10-28 18:58:04 +01:00
DevTekVE
de7acc5466 ci: integrate Discourse notifications and refactor notification logic (#1435)
* ci: integrate Discourse notifications and refactor notification logic

- Replaced Discord webhook notifications with Discourse topic updates.
- Introduced reusable `post-to-discourse` composite action.
- Added `test-discourse.yaml` workflow for debugging and verification.

* ci: adjust notification dependencies and prepare_strategy reference

- Updated `notify` step to depend on `prepare_strategy` instead of `build`.
- Adjusted variable references to use `prepare_strategy` outputs.

* Forcing debug

* ci: update environment variable references and add commit information

- Switched `PUBLIC_REPO_URL` source to environment variable for consistency.
- Added commit SHA variables to enhance template generation logic.

* more tweaks!

* more tweaks!

* bad bot lmao

* Test?

* i mean....

* i mean....

* getting there

* testing the if

* testing the if

* ci: re-enable notify steps for prebuilt workflow

- Uncommented `build` and `publish` dependencies.
- Restored conditional logic to trigger only for relevant events.

* ci: enhance Discourse action to support new topic creation

- Added support for creating new topics with `category-id` and `title`.
- Improved input validation and response handling for flexibility.

* ci: improve conditions for prebuilt workflow notifications

- Refined `if` clause to ensure branches in `DEV_FEEDBACK_NOTIFICATION_BRANCHES` are targeted.
- Adjusted logic for accurate topic ID mapping in Discourse integration.

* forgot to rename
2025-10-28 16:01:21 +01:00
Nayan
e4aada10a4 Bug: Model UI Crash Fix (#1431)
Model UI Crash Fix
2025-10-26 21:43:30 -04:00
James Vecellio-Grant
b460d5804c LiveLocationKalman: skip tests on unsupported msgq (#1407)
* locationd llk: skip tests on unsupported msgq

* Update sunnypilot/selfdrive/locationd/tests/test_locationd.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-25 23:47:37 -04:00
James Vecellio-Grant
eecb8e5c19 models: bump model json to v8 (#1430)
models: bump model json to v8 post release

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-25 22:55:26 -04:00
Jason Wen
1a4ea66987 version: bump to 2025.002.000 2025-10-25 22:45:17 -04:00
Jason Wen
c1e15e5544 changelog: add new contributor entry 2025-10-25 01:00:46 -04:00
MuskratGG
3a45fff1b9 ui: openpilot Longitudinal Control → sunnypilot Longitudinal Control (#1422)
* Update developer_panel.cc

Changed mentions of "openpilot Longitudinal Control" to "sunnypilot Longitudinal Control" to align with other UI elements pointing users towards enabling "sunnypilot Longitudinal Control"

* Update warning message for longitudinal control

* more

* a bit more

* slightly more

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-24 21:09:01 -04:00
github-actions[bot]
ae9bd39883 [bot] Update Python packages (#1428)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-24 17:04:27 -04:00
Jason Wen
43e7d87176 version: more release branches (#1427) 2025-10-24 15:34:50 -04:00
github-actions[bot]
432c6050ed [bot] Update Python packages (#1338)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-24 15:28:08 -04:00
Jason Wen
4e3b1f1f6b interface: add is_release flag to get_params_sp (#1426)
* interface: add `is_release` flag to `get_params_sp`

* split and rename

* debump
2025-10-24 14:56:24 -04:00
Jason Wen
5d47ffdb8a Speed Limit Assist: Disable for Rivian (#1421)
* Speed Limit Assist: Disable for Tesla in release

* add test

* unused

* use constant

* eh

* flip

* universal it

* check release state and align in tests

* use this

* eh

* update changelog

* Speed Limit Assist: Disable for Rivian

* desc

* changelog
2025-10-23 03:48:04 -04:00
Jason Wen
1c89e2b885 Speed Limit Assist: Disable for Tesla in release (#1418)
* Speed Limit Assist: Disable for Tesla in release

* add test

* unused

* use constant

* eh

* flip

* universal it

* check release state and align in tests

* use this

* eh

* update changelog
2025-10-23 03:26:32 -04:00
Jason Wen
c552567ada ui: increase minimum button width in ButtonParamControlSP (#1419) 2025-10-23 01:08:19 -04:00
Jason Wen
7097e69aa3 Speed Limit Assist: generalize availability helper (#1416)
* init infra to disable sla in certain conditions

* a bit more

* in another PR

* in another PR

* since when?

* start here
2025-10-22 11:17:02 -04:00
Jason Wen
657ff0f8ec ui: refine ICBM description handling and availability logic (#1414)
* ui: refine ICBM description handling and availability logic

* car -> platform

* retain
2025-10-22 00:09:10 -04:00
Jason Wen
641af6d7e7 changelog: more new contributors! (#1413) 2025-10-21 17:55:27 -04:00
Jason Wen
f57de1c5b2 version: a new beginning (#1411)
* version: a new beginning

* changelog

* singular

* show ours

* actual

* readjust

* updated

* more

* official spelling

* more

* sync

* fix

* send it

* push

* we never had this lol

* syncs
2025-10-21 17:12:57 -04:00
Jason Wen
cb5d120136 FCA: update minEnableSpeed and LKAS control logic (#1386)
* FCA: update minEnableSpeed and LKAS control logic

* bump
2025-10-21 14:22:37 -04:00
Jason Wen
c85b6a0d1c branches: track sunnypilot release branches separately (#1409)
* branches: track sunnypilot release branches separately

* more remotes for legacy support

* bruh

* revert
2025-10-21 00:53:16 -04:00
Jason Wen
025a930ce8 ui: update longitudinal-related settings handling (#1401)
* ui: update ICBM-related settings handling

* oops

* oops

* single location

* some more

* fix cruise toggles

* always init true

* check this

* nah

* should be this
2025-10-18 04:04:48 -04:00
Jason Wen
523c92c6fe Speed Limit Assist: lower preActive timer for Non PCM Longitudinal and ICBM cars (#1403)
5 seconds preActive for non pcm long now
2025-10-17 23:41:33 -04:00
Jason Wen
72282f2d2e Speed Limit Assist: update events handling (#1400)
* Speed Limit Assist: update active event handling

* ok no more for non pcm long it was annoying

* 5 seconds preActive for non pcm long now

* Revert "5 seconds preActive for non pcm long now"

This reverts commit dfcc601035.

* dynamic alert size

* do the same here

* lint
2025-10-17 23:30:06 -04:00
Jason Wen
2825c00fcc controlsd: update lateral delay param in a separate thread (#1402) 2025-10-17 22:53:31 -04:00
Nayan
063aa994d2 ui: Resize E2E Alerts (#1396)
because people be enabling ALL THE UI

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-16 01:03:45 -04:00
Jason Wen
50462a1d01 E2E Alert: universal state machine (#1395)
* E2E Helper: universal state machine

* not used

* rename

* 10 frames for both

* time based

* magic

* lead depart: only arm if we have a confirmed close lead for over a second after allowing alert

* less

* shorter trigger

* lol

* always update
2025-10-16 00:55:17 -04:00
Jason Wen
437726b348 Speed Limit Mode: only cleanup param if Assist was selected (#1393)
Speed Limit Mode: only cleanup param if it was Assist
2025-10-15 18:05:50 -04:00
Nayan
9e6af5ba74 ui: Adjust UI Elements to account for Sidebar & Dev UI (#1390)
* resize & reposition

* Apply suggestion from @sunnyhaibin

* sir, this is Wendy's

* this is still a Wendy's

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-15 17:40:52 -04:00
Nayan
99bd9075d5 ui: Fix Onroad Screen-Off default param (#1389)
Change defaults

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-15 17:18:31 -04:00
Jason Wen
c438aeb5a5 ui: check for updated message before updating states in HUD (#1392) 2025-10-15 16:47:00 -04:00
Jason Wen
f1ca81debf ui: chevron should always be on top of driving path (#1391) 2025-10-15 16:20:31 -04:00
Jason Wen
d7e1c42c2b ui: move Dynamic Experimental Control (DEC) toggle to Longitudinal panel (#1388)
- Implemented a new toggle for enabling Dynamic Experimental Control (DEC) in longitudinal settings.
- Removed previous implementation for DEC from general settings.
- Updated accessibility based on longitudinal control status.
2025-10-15 12:25:23 -04:00
Jason Wen
6d51d64285 interfaces: clean up unsupported params during initialization (#1385)
* interfaces: clean up unsupported params during initialization

* fix

* logging and no DEC when no long

* ui

* ui
2025-10-15 09:46:53 -04:00
Jason Wen
e0ccc175e4 liveMapDataSP: improve speed limit validation logic (#1383) 2025-10-15 00:59:40 -04:00
James Vecellio-Grant
734151f59b Reapply "capnp: consolidate TurnDirection enum" (#1376) (#1382)
* Reapply "capnp: consolidate TurnDirection enum" (#1376)

This reverts commit 339bc0b8b3.

* cache it

* format

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-14 23:32:10 -04:00
Nayan
9a14baac4d Green Light and Lead Departure alerts improvements (#1381) 2025-10-14 20:01:42 -04:00
Jason Wen
d3e3628a95 ui: only draw ahead speed limit if it's parsed from OSM (#1380) 2025-10-14 11:40:42 -04:00
Nayan
fec6382b96 UI: Fix Speed Limit Assist (SLA) Translations (#1379)
Fix SLA Translations
2025-10-14 11:29:27 -04:00
Jason Wen
4bd020e92b Speed Limit Assist: audible alerts for certain states (#1378) 2025-10-14 09:19:26 -04:00
Jason Wen
7f5342f378 soundd: custom audible alerts (#1377)
* Revert "capnp: consolidate TurnDirection enum (#1370)"

This reverts commit 7229c7541e.

* soundd: custom audible alerts

* comment
2025-10-14 01:13:20 -04:00
Jason Wen
339bc0b8b3 Revert "capnp: consolidate TurnDirection enum" (#1376)
Revert "capnp: consolidate TurnDirection enum (#1370)"

This reverts commit 7229c7541e.
2025-10-14 00:19:21 -04:00
Jason Wen
59c64acc29 Subaru: Stop and Go support (beta) (#1375)
* Subaru: Stop and Go auto-resume support

* bump

* bump

* fix

* bump

* fix init

* wat

* use just standstill for now

* Revert "use just standstill for now"

This reverts commit f72cce6892.

* bump

* bump

* fix it

* only send at 10

* bump

* fix type

* forget about planner resume, it sucks

* try to send off_accel

* still need it

* always send

* disable safety checks for now

* same

* more

* all the time for both

* don't need i guess

* bump

* try 15 frames per try

* all should have it

* try 3 for all

* use throttle for all preglobal?

* bump

* bump

* separate thresholds between preglobal and global

* longer wait before sending

* shorter time but immediately resend

* quick

* new timeout

* about to cry

* same thing but another try

* no need

* round 3

* try 1.4

* lower!

* 1.2

* last try

* beta asf

* bump
2025-10-13 22:26:47 -04:00
James Vecellio-Grant
7229c7541e capnp: consolidate TurnDirection enum (#1370)
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-13 22:02:53 -04:00
Kumar
39e73cc46e ui: add ModelRendererSP::draw (#1372)
* ModelRendererSP::draw

* match

* less

* huh?

* unused

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-13 21:45:59 -04:00
Jason Wen
285fd97606 ui: only draw speedCluster speed over "MAX" when ICBM is enabled (#1374) 2025-10-13 20:36:24 -04:00
Jason Wen
e5f1f86ac2 params: helper to clamp out-of-range int params (#1373)
* params: helpers to clamp out-of-range values

* lint

* inline

* fix access

* actually fix the param

* inherit them

* more lint
2025-10-13 19:37:58 -04:00
Jason Wen
7e03277962 ui: bigger cluster set speed fonts when ICBM is active (#1369) 2025-10-13 03:06:39 -04:00
Jason Wen
bd9bb74d03 custom cereal: fix formatting (#1367) 2025-10-13 01:40:22 -04:00
Kirito3481
7d54b58b8d ui: Update Korean translations (#1359)
Update ko-kr translations

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-13 00:19:24 -04:00
Jason Wen
68d059fd5d ui: ensure Cruise panel widget is reset when hideEvent (#1366) 2025-10-12 23:48:58 -04:00
Nayan
728108f97f ui: Optimize Param Read for Onroad UI (#1365)
* Move params to uiscene

* more

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-12 23:36:58 -04:00
Kumar
cb6fa622ee Visuals: Move custom chevron info to sunnypilot/qt (#1066)
* refactor: move to sp ui

* pr suggestion

* no need to check pcm just oplong

* no color, big front, long check

* Fix Rainbow Mode & Y Positioning

* Move param to uiscene

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: nayan <nayan8teen@gmail.com>
2025-10-12 23:11:23 -04:00
Nayan
5b29fd0f2c UI: Onroad Screen Timeout Fixes (#1364)
* Screen Timeout Fixes

* rename

* auto type

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-12 12:12:52 -04:00
Nayan
6f42bbab18 Reapply "UI: Make Always Offroad more accessible" (#1327) (#1361)
* UI: Make Always Offroad more accessible

* conditional - based on offroad

* no need to delete

* account for always offroad

* fix offroad transition

* do this inside updateState on every invoke

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-12 01:59:03 -04:00
Nayan
b89393a5a2 UI: Blinker Size & State Fix (#1363)
Blinker Size & State Fix

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-12 00:34:15 -04:00
Nayan
1e5758e712 ui: Better E2E Alert UI Positioning (#1355)
Better E2E Alert UI Positioning
2025-10-12 00:21:38 -04:00
James Vecellio-Grant
974a7a3f7d ui: improve toggle states in Developer panel (#1352)
ui: quickboot toggle race condition

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-10-11 02:47:45 -04:00
Jason Wen
fe6edda23a Revert "UI: Make Always Offroad more accessible (#1327)" (#1358)
Reverts #1327
2025-10-11 01:52:33 -04:00
Jason Wen
17e25f78b4 ui: dynamic ICBM status (#1357)
* ui: dynamic ICBM status

* straight up pls
2025-10-10 17:00:44 -04:00
Nayan
9b92cdd2cc ui: fix standstill timer reset when offroad (#1351)
Standstill timer fix
2025-10-09 23:57:09 -04:00
141 changed files with 3097 additions and 1040 deletions

View File

@@ -3,3 +3,4 @@ REGIST
PullRequest
cancelled
FOF
NoO

View File

@@ -29,11 +29,11 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target: /^(?!master-tici$).*/
target: /^(?!master$).*/
exclude: /sunnypilot:.*/
change-to: ${{ github.base_ref }}
already-exists-action: close_this
already-exists-comment: "Your PR should be made against the `master-tici` branch"
already-exists-comment: "Your PR should be made against the `master` branch"
update-pr-labels:
name: Update fork's PR Labels

View File

@@ -5,7 +5,7 @@ on:
workflow_dispatch:
env:
BASE_IMAGE: sunnypilot-tici-base
BASE_IMAGE: sunnypilot-base
DOCKER_REGISTRY: ghcr.io/sunnypilot
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $DOCKER_REGISTRY/$BASE_IMAGE:latest /bin/bash -c

View File

@@ -3,7 +3,7 @@ name: cereal validation
on:
push:
branches:
- master-tici
- master
pull_request:
paths:
- 'cereal/**'
@@ -16,7 +16,7 @@ on:
type: string
concurrency:
group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:

View File

@@ -31,7 +31,7 @@ jobs:
strategy:
fail-fast: false
matrix: ${{fromJSON(needs.setup.outputs.ci_runs)}}
uses: sunnypilot/sunnypilot/.github/workflows/ci_weekly_run.yaml@master-tici
uses: sunnypilot/sunnypilot/.github/workflows/ci_weekly_run.yaml@master
with:
run_number: ${{ matrix.run_number }}

View File

@@ -12,6 +12,6 @@ concurrency:
jobs:
selfdrive_tests:
uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master-tici
uses: sunnypilot/sunnypilot/.github/workflows/selfdrive_tests.yaml@master
with:
run_number: ${{ inputs.run_number }}

View File

@@ -15,7 +15,7 @@ runs:
scons -j$(nproc) --cache-populate"
- name: Save scons cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master-tici'
if: github.ref == 'refs/heads/master'
with:
path: .ci_cache/scons_cache
key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}

View File

@@ -3,7 +3,7 @@ name: docs
on:
push:
branches:
- master-tici
- master
pull_request:
workflow_call:
inputs:
@@ -12,7 +12,7 @@ on:
required: true
type: string
concurrency:
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
@@ -35,13 +35,13 @@ jobs:
# Push to docs.comma.ai
- uses: actions/checkout@v4
if: github.ref == 'refs/heads/master-tici' && github.repository == 'sunnypilot/sunnypilot'
if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot'
with:
path: openpilot-docs
ssh-key: ${{ secrets.OPENPILOT_DOCS_KEY }}
repository: sunnypilot/sunnypilot-docs
- name: Push
if: github.ref == 'refs/heads/master-tici' && github.repository == 'sunnypilot/sunnypilot'
if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot'
run: |
set -x

View 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

View File

@@ -24,11 +24,11 @@ jobs:
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master-tici
ref: master
wait-interval: 30
running-workflow-name: 'build prebuilt'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build __nightly).*).)*$
check-regexp: ^((?!.*(build master-ci).*).)*$
- uses: actions/checkout@v4
with:
submodules: true

View File

@@ -19,7 +19,6 @@ jobs:
contents: write
pull-requests: write
runs-on: ubuntu-latest
if: False
steps:
- uses: release-drafter/release-drafter@v6
with:

View File

@@ -10,7 +10,7 @@ jobs:
env:
ImageOS: ubuntu24
container:
image: ghcr.io/sunnypilot/sunnypilot-tici-base:latest
image: ghcr.io/sunnypilot/sunnypilot-base:latest
runs-on: ubuntu-latest
if: github.repository == 'sunnypilot/sunnypilot'
permissions:
@@ -25,7 +25,7 @@ jobs:
if: ${{ github.event_name == 'schedule' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master-tici
ref: master
wait-interval: 30
running-workflow-name: 'build __nightly'
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,7 +6,7 @@ on:
workflow_dispatch:
env:
BASE_IMAGE: sunnypilot-tici-base
BASE_IMAGE: sunnypilot-base
BUILD: release/ci/docker_build_sp.sh base
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
@@ -28,7 +28,7 @@ jobs:
title: "[bot] Update translations"
body: "Automatic PR from repo-maintenance -> update_translations"
branch: "update-translations"
base: "master-tici"
base: "master"
delete-branch: true
labels: bot
@@ -36,7 +36,7 @@ jobs:
name: package_updates
runs-on: ubuntu-latest
container:
image: ghcr.io/sunnypilot/sunnypilot-tici-base:latest
image: ghcr.io/sunnypilot/sunnypilot-base:latest
if: github.repository == 'sunnypilot/sunnypilot'
steps:
- uses: actions/checkout@v4
@@ -68,7 +68,7 @@ jobs:
commit-message: Update Python packages
title: '[bot] Update Python packages'
branch: auto-package-updates
base: master-tici
base: master
delete-branch: true
body: 'Automatic PR from repo-maintenance -> package_updates'
labels: bot

View File

@@ -3,7 +3,7 @@ name: selfdrive
on:
push:
branches:
- master-tici
- master
pull_request:
workflow_dispatch:
workflow_call:
@@ -14,12 +14,12 @@ on:
type: string
concurrency:
group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:
PYTHONWARNINGS: error
BASE_IMAGE: sunnypilot-tici-base
BASE_IMAGE: sunnypilot-base
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
@@ -68,10 +68,10 @@ jobs:
if: github.repository == 'sunnypilot/sunnypilot'
timeout-minutes: 3
run: |
if [ "${{ github.ref }}" != "refs/heads/master-tici" ]; then
git fetch origin master-tici:refs/remotes/origin/master-tici
if [ "${{ github.ref }}" != "refs/heads/master" ]; then
git fetch origin master:refs/remotes/origin/master
SUBMODULE_PATHS=$(git diff origin/master-tici HEAD --name-only | grep -E '^[^/]+$' | while read path; do
SUBMODULE_PATHS=$(git diff origin/master HEAD --name-only | grep -E '^[^/]+$' | while read path; do
if git ls-files --stage "$path" | grep -q "^160000"; then
echo "$path"
fi
@@ -97,7 +97,7 @@ jobs:
with:
submodules: true
- name: Setup docker push
if: github.ref == 'refs/heads/master-tici' && github.event_name != 'pull_request' && github.repository == 'sunnypilot/sunnypilot'
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'sunnypilot/sunnypilot'
run: |
echo "PUSH_IMAGE=true" >> "$GITHUB_ENV"
$DOCKER_LOGIN
@@ -130,7 +130,7 @@ jobs:
HOMEBREW_DISPLAY_INSTALL_TIMES: 1
- name: Save Homebrew cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master-tici'
if: github.ref == 'refs/heads/master'
with:
path: ~/Library/Caches/Homebrew
key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
@@ -148,7 +148,7 @@ jobs:
run: . .venv/bin/activate && scons -j$(nproc)
- name: Save scons cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master-tici'
if: github.ref == 'refs/heads/master'
with:
path: /tmp/scons_cache
key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
@@ -307,5 +307,5 @@ jobs:
- name: Upload Test Report
uses: actions/upload-artifact@v4
with:
name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && 'master-tici' || github.event.number }}
name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/test_ui/report_1/screenshots

View File

@@ -14,7 +14,7 @@ on:
upstream_branch:
description: 'Upstream branch to build from'
required: true
default: 'master-tici'
default: 'master'
type: string
custom_name:
description: 'Custom name for the model (no date, only name)'
@@ -35,7 +35,7 @@ on:
upstream_branch:
description: 'Upstream branch to build from'
required: true
default: 'master-tici'
default: 'master'
type: string
custom_name:
description: 'Custom name for the model (no date, only name)'

View File

@@ -8,14 +8,14 @@ env:
PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot"
# Branch configurations
STAGING_C3_SOURCE_BRANCH: 'master-tici' # vars.STAGING_C3_SOURCE_BRANCH could be used, set on repo settings.
STAGING_SOURCE_BRANCH: 'master'
# Runtime configuration
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
on:
push:
branches: [ master-tici ]
branches: [ master, master-dev ]
tags: [ 'release/*' ]
pull_request_target:
types: [ labeled ]
@@ -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
@@ -138,7 +138,7 @@ jobs:
# for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden.
restore-keys: |
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_C3_SOURCE_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_SOURCE_BRANCH }}
scons-${{ runner.os }}-${{ runner.arch }}
- name: Set environment variables
@@ -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

View File

@@ -1,31 +1,30 @@
name: Build dev-c3-new
name: Build dev
env:
DEFAULT_SOURCE_BRANCH: "master-tici"
DEFAULT_TARGET_BRANCH: "master-dev-c3-new"
PR_LABEL: "dev-c3"
DEFAULT_SOURCE_BRANCH: "master"
DEFAULT_TARGET_BRANCH: "master-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'
on:
push:
branches:
- master-tici
- master
pull_request_target:
types: [ labeled ]
branches:
- 'master-tici'
- 'master'
workflow_dispatch:
inputs:
source_branch:
description: 'Source branch to reset from'
required: true
default: 'master-tici'
default: 'master'
type: string
target_branch:
description: 'Target branch to reset and squash into'
required: true
default: 'master-dev-c3-new'
default: 'master-dev'
type: string
cancel_in_progress:
description: 'Cancel any in-progress runs of this workflow'
@@ -40,7 +39,11 @@ concurrency:
jobs:
reset-and-squash:
runs-on: ubuntu-latest
if: False
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 == 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
with:
@@ -51,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-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3'))))
|| (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
@@ -114,8 +117,8 @@ jobs:
run: |
# Use GitHub API to get PRs with specific label, ordered by creation date
PR_LIST=$(gh api graphql -f query='
query($label:String!) {
search(query: $label, type:ISSUE, first:100) {
query($search_query:String!) {
search(query: $search_query, type:ISSUE, first:40) {
nodes {
... on PullRequest {
number
@@ -145,7 +148,7 @@ jobs:
}
}
}
}' -F label="is:pr is:open label:${PR_LABEL} 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

View 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"

View File

@@ -2,19 +2,19 @@ name: "ui preview"
on:
push:
branches:
- master-tici
- master
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]
branches:
- 'master-tici'
- 'master'
paths:
- 'selfdrive/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create UI Report"
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && 'master-tici' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master-tici' && github.sha || github.event.pull_request.head.sha }}
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}"
jobs:
@@ -55,7 +55,7 @@ jobs:
name: report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master-tici ui
- name: Getting master ui
uses: actions/checkout@v4
with:
repository: sunnypilot/ci-artifacts
@@ -63,8 +63,8 @@ jobs:
path: ${{ github.workspace }}/master_ui
ref: openpilot_master_ui
- name: Saving new master-tici ui
if: github.ref == 'refs/heads/master-tici' && github.event_name == 'push'
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui
run: |
git checkout --orphan=new_master_ui
@@ -93,9 +93,9 @@ jobs:
for ((i=0; i<${#A[*]}; i=i+1));
do
# Check if the master-tici file exists
# Check if the master file exists
if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then
# This is a new file in PR UI that doesn't exist in master-tici
# This is a new file in PR UI that doesn't exist in master
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
DIFF="${DIFF}<table>"
@@ -118,7 +118,7 @@ jobs:
DIFF="${DIFF}<table>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> master-tici <img src=\"https://raw.githubusercontent.com/sunnypilot/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>"
DIFF="${DIFF} <td> master <img src=\"https://raw.githubusercontent.com/sunnypilot/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>"
DIFF="${DIFF} <td> proposed <img src=\"https://raw.githubusercontent.com/sunnypilot/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
DIFF="${DIFF}</tr>"

1080
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/sunnypilot/sunnypilot-tici-base:latest
FROM ghcr.io/sunnypilot/sunnypilot-base:latest
ENV PYTHONUNBUFFERED=1

View File

@@ -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
![](https://dcbadge.vercel.app/api/server/wRW3meAgtx?style=flat) ![Discord Shield](https://discordapp.com/api/guilds/880416502577266699/widget.png?style=shield)
## 💭 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-tici` 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,27 +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-tici.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-tici`
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging`
| Branch | Installation URL |
### Recommended Branches
| Branch | Installation URL |
|:---------------:|:---------------------------------------------:|
| `staging-tici` | `https://staging-tici.sunnypilot.ai` |
| `release` | `https://release.sunnypilot.ai` |
| `staging` | `https://staging.sunnypilot.ai` |
| `dev` | `https://dev.sunnypilot.ai` |
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
| `release-tici` | **Not yet available**. |
> [!TIP]
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging-tici'.
> 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>

View File

@@ -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,13 +446,13 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
}
enum TurnDirection {
none @0;
turnLeft @1;
turnRight @2;
}
}
struct CustomReserved10 @0xcb9fd56c7057593a {
}

View File

@@ -1 +1 @@
#define DEFAULT_MODEL "Firehose (Default)"
#define DEFAULT_MODEL "TCPv3 + gWMv9 (Default)"

View File

@@ -94,7 +94,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_StorageMissing", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}},
@@ -155,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"}},
@@ -206,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"}},

View File

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

View File

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

View File

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

View File

@@ -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>&nbsp;|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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Trailblazer 2021-22">Buy Here</a></sub></details>|||
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?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>||

View File

@@ -39,7 +39,7 @@ All of these are examples of good PRs:
### First contribution
[Projects / openpilot bounties](https://github.com/orgs/commaai/projects/26/views/1?pane=info) is the best place to get started and goes in-depth on what's expected when working on a bounty.
There's lot of bounties that don't require a comma 3/3X or a car.
There's lot of bounties that don't require a comma 3X or a car.
## Pull Requests

View File

@@ -1,11 +1,11 @@
# connect to a comma 3/3X
# connect to a comma 3X
A comma 3/3X is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
A comma 3X is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
## Serial Console
On both the comma three and 3X, the serial console is accessible from the main OBD-C port.
Connect the comma 3/3X to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
Connect the comma 3X to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
On the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect.
@@ -45,7 +45,7 @@ In order to use ADB on your device, you'll need to perform the following steps u
* Here's an example command for connecting to your device using its tethered connection: `adb connect 192.168.43.1:5555`
> [!NOTE]
> The default port for ADB is 5555 on the comma 3/3X.
> The default port for ADB is 5555 on the comma 3X.
For more info on ADB, see the [Android Debug Bridge (ADB) documentation](https://developer.android.com/tools/adb).

View File

@@ -8,7 +8,7 @@ Replaying is a critical tool for openpilot development and debugging.
Just run `tools/replay/replay --demo`.
## Replaying CAN data
*Hardware required: jungle and comma 3/3X*
*Hardware required: jungle and comma 3X*
1. Connect your PC to a jungle.
2.

View File

@@ -3,7 +3,7 @@
In 30 minutes, we'll get an openpilot development environment set up on your computer and make some changes to openpilot's UI.
And if you have a comma 3/3X, we'll deploy the change to your device for testing.
And if you have a comma 3X, we'll deploy the change to your device for testing.
## 1. Set up your development environment

View File

@@ -21,7 +21,7 @@ nav:
- What is openpilot?: getting-started/what-is-openpilot.md
- How-to:
- Turn the speed blue: how-to/turn-the-speed-blue.md
- Connect to a comma 3/3X: how-to/connect-to-comma.md
- Connect to a comma 3X: how-to/connect-to-comma.md
# - Make your first pull request: how-to/make-first-pr.md
#- Replay a drive: how-to/replay-a-drive.md
- Concepts:

2
panda

Submodule panda updated: 2a7914fa5a...69ab12ee2a

View File

@@ -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"

View File

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

View File

@@ -31,25 +31,13 @@ while read hash submodule ref; do
exit 1
fi
else
# Check against master-tici for specific submodules, master for others
if [ "$submodule" = "opendbc_repo" ] || [ "$submodule" = "panda" ]; then
git -C $submodule fetch --depth 100 origin master-tici
remote_hash=$(git -C $submodule rev-parse FETCH_HEAD)
if git -C $submodule merge-base --is-ancestor $hash $remote_hash; then
echo "$submodule ok"
else
echo "$submodule: $hash is not on master-tici"
exit 1
fi
git -C $submodule fetch --depth 100 origin master
git -C $submodule branch -r --contains $hash | grep "origin/master"
if [ "$?" -eq 0 ]; then
echo "$submodule ok"
else
git -C $submodule fetch --depth 100 origin master
git -C $submodule branch -r --contains $hash | grep "origin/master"
if [ "$?" -eq 0 ]; then
echo "$submodule ok"
else
echo "$submodule: $hash is not on master"
exit 1
fi
echo "$submodule: $hash is not on master"
exit 1
fi
fi
done <<< $(git submodule status --recursive)

View File

@@ -1,8 +1,8 @@
if [ "$1" = "base" ]; then
export DOCKER_IMAGE=sunnypilot-tici-base
export DOCKER_IMAGE=sunnypilot-base
export DOCKER_FILE=Dockerfile.sunnypilot_base
elif [ "$1" = "prebuilt" ]; then
export DOCKER_IMAGE=sunnypilot-tici-prebuilt
export DOCKER_IMAGE=sunnypilot-prebuilt
export DOCKER_FILE=Dockerfile.sunnypilot
else
echo "Invalid docker build image: '$1'"

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@@ -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):

View File

@@ -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]

View File

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

View File

@@ -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],

View File

@@ -99,11 +99,6 @@ def main() -> None:
cloudlog.event("pandad.flash_and_connect", count=count)
params.remove("PandaSignatures")
# TODO: remove this in the next AGNOS
# wait until USB is up before counting
if time.monotonic() < 60.:
no_internal_panda_count = 0
# Handle missing internal panda
if no_internal_panda_count > 0:
if no_internal_panda_count == 3:

View File

@@ -22,8 +22,6 @@ class TestPandad:
if len(Panda.list()) == 0:
self._run_test(60)
self.spi = HARDWARE.get_device_type() != 'tici'
def teardown_method(self):
managed_processes['pandad'].stop()
@@ -106,11 +104,9 @@ class TestPandad:
# - 0.2s pandad -> pandad
# - plus some buffer
print("startup times", ts, sum(ts) / len(ts))
assert 0.1 < (sum(ts)/len(ts)) < (0.7 if self.spi else 5.0)
assert 0.1 < (sum(ts)/len(ts)) < 0.7
def test_protocol_version_check(self):
if not self.spi:
pytest.skip("SPI test")
# flash old fw
fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")
self._flash_bootstub_and_test(fn, expect_mismatch=True)

View File

@@ -6,7 +6,6 @@ import random
import cereal.messaging as messaging
from cereal.services import SERVICE_LIST
from openpilot.system.hardware import HARDWARE
from openpilot.selfdrive.test.helpers import with_processes
from openpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad, send_random_can_messages
@@ -16,8 +15,6 @@ JUNGLE_SPAM = "JUNGLE_SPAM" in os.environ
class TestBoarddSpi:
@classmethod
def setup_class(cls):
if HARDWARE.get_device_type() == 'tici':
pytest.skip("only for spi pandas")
os.environ['STARTED'] = '1'
os.environ['SPI_ERR_PROB'] = '0.001'
if not JUNGLE_SPAM:

View File

@@ -29,10 +29,6 @@
"text": "Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support.",
"severity": 1
},
"Offroad_StorageMissing": {
"text": "NVMe drive not mounted.",
"severity": 1
},
"Offroad_CarUnrecognized": {
"text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.",
"severity": 0

View File

@@ -21,10 +21,10 @@ from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck
from openpilot.selfdrive.selfdrived.state import StateMachine
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert
from openpilot.system.hardware import HARDWARE
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
@@ -44,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)
@@ -131,17 +132,17 @@ 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)
# some comma three with NVMe experience NVMe dropouts mid-drive that
# cause loggerd to crash on write, so ignore it only on that platform
self.ignored_processes = set()
nvme_expected = os.path.exists('/dev/nvme0n1') or (not os.path.isfile("/persist/comma/living-in-the-moment"))
if HARDWARE.get_device_type() == 'tici' and nvme_expected:
self.ignored_processes = {'loggerd', 'mapd'}
self.ignored_processes = {'mapd', }
# Determine startup event
is_remote = build_metadata.openpilot.comma_remote or build_metadata.openpilot.sunnypilot_remote
@@ -305,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']):

View File

@@ -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):

View File

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

View File

@@ -12,7 +12,7 @@ public:
explicit DeveloperPanel(SettingsWindow *parent);
void showEvent(QShowEvent *event) override;
private:
protected:
Params params;
ParamControl* adbToggle;
ParamControl* joystickToggle;

View File

@@ -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.");

View File

@@ -4,9 +4,6 @@
#include <map>
#include "selfdrive/ui/qt/util.h"
#ifdef SUNNYPILOT
#include "selfdrive/ui/sunnypilot/ui.h"
#endif
void OnroadAlerts::updateState(const UIState &s) {
Alert a = getAlert(*(s.sm), s.scene.started_frame);
@@ -76,12 +73,6 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) {
}
QRect r = QRect(0 + margin, height() - h + margin, width() - margin*2, h - margin*2);
#ifdef SUNNYPILOT
const int dev_ui_info = uiStateSP()->scene.dev_ui_info;
const int adjustment = dev_ui_info > 1 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 30 : 0;
r = QRect(0 + margin, height() - h + margin - adjustment, width() - margin*2, h - margin*2);
#endif
QPainter p(this);
// draw background + gradient

View File

@@ -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());
@@ -34,7 +34,6 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
drawLead(painter, lead_two, lead_vertices[1], surface_rect);
}
}
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
painter.restore();
}
@@ -174,173 +173,6 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
(1 - t) * start.alphaF() + t * end.alphaF());
}
void ModelRenderer::drawLeadStatus(QPainter &painter, int height, int width) {
auto *s = uiState();
auto &sm = *(s->sm);
if (!sm.alive("radarState")) return;
const auto &radar_state = sm["radarState"].getRadarState();
const auto &lead_one = radar_state.getLeadOne();
const auto &lead_two = radar_state.getLeadTwo();
// Check if we have any active leads
bool has_lead_one = lead_one.getStatus();
bool has_lead_two = lead_two.getStatus();
if (!has_lead_one && !has_lead_two) {
// Fade out status display
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
if (lead_status_alpha <= 0.0f) return;
} else {
// Fade in status display
lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f);
}
// Draw status for each lead vehicle under its chevron
if (true) {
drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1");
}
if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) {
drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2");
}
}
void ModelRenderer::drawLeadStatusAtPosition(QPainter &painter,
const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos,
int height, int width,
const QString &label) {
float d_rel = lead_data.getDRel();
float v_rel = lead_data.getVRel();
auto *s = uiState();
auto &sm = *(s->sm);
float v_ego = sm["carState"].getCarState().getVEgo();
int chevron_data = std::atoi(Params().get("ChevronInfo").c_str());
// Calculate chevron size (same logic as drawLead)
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35;
QFont content_font = painter.font();
content_font.setPixelSize(35);
content_font.setBold(true);
painter.setFont(content_font);
QFontMetrics fm(content_font);
bool is_metric = s->scene.is_metric;
QStringList text_lines;
const int chevron_types = 3;
const int chevron_all = chevron_types + 1; // All metrics (value 4)
QStringList chevron_text[chevron_types];
int position;
float val;
// Distance display (chevron_data == 1 or all)
if (chevron_data == 1 || chevron_data == chevron_all) {
position = 0;
val = std::max(0.0f, d_rel);
QString distance_unit = is_metric ? "m" : "ft";
if (!is_metric) {
val *= 3.28084f; // Convert meters to feet
}
chevron_text[position].append(QString::number(val, 'f', 0) + " " + distance_unit);
}
// Absolute velocity display (chevron_data == 2 or all)
if (chevron_data == 2 || chevron_data == chevron_all) {
position = (chevron_data == 2) ? 0 : 1;
val = std::max(0.0f, (v_rel + v_ego) * (is_metric ? static_cast<float>(MS_TO_KPH) : static_cast<float>(MS_TO_MPH)));
chevron_text[position].append(QString::number(val, 'f', 0) + " " + (is_metric ? "km/h" : "mph"));
}
// Time-to-contact display (chevron_data == 3 or all)
if (chevron_data == 3 || chevron_data == chevron_all) {
position = (chevron_data == 3) ? 0 : 2;
val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f;
QString ttc_str = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---";
chevron_text[position].append(ttc_str);
}
// Collect all non-empty text lines
for (int i = 0; i < chevron_types; ++i) {
if (!chevron_text[i].isEmpty()) {
text_lines.append(chevron_text[i]);
}
}
// If no text to display, return early
if (text_lines.isEmpty()) {
return;
}
// Text box dimensions
float str_w = 150; // Width of text area
float str_h = 45; // Height per line
// Position text below chevron, centered horizontally
float text_x = chevron_pos.x() - str_w / 2;
float text_y = chevron_pos.y() + sz + 15;
// Clamp to screen bounds
text_x = std::clamp(text_x, 10.0f, (float)width - str_w - 10);
// Shadow offset
QPoint shadow_offset(2, 2);
// Draw each line of text with shadow
for (int i = 0; i < text_lines.size(); ++i) {
if (!text_lines[i].isEmpty()) {
QRect textRect(text_x, text_y + (i * str_h), str_w, str_h);
// Draw shadow
painter.setPen(QColor(0x0, 0x0, 0x0, (int)(200 * lead_status_alpha)));
painter.drawText(textRect.translated(shadow_offset.x(), shadow_offset.y()),
Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]);
// Determine text color based on content and danger level
QColor text_color;
// Check if this is a distance line (contains 'm' or 'ft')
if (text_lines[i].contains("m") || text_lines[i].contains("ft")) {
if (d_rel < 20.0f) {
text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - danger
} else if (d_rel < 40.0f) {
text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution
} else {
text_color = QColor(80, 255, 120, (int)(255 * lead_status_alpha)); // Green - safe
}
}
// Enhanced color coding for time-to-contact
else if (text_lines[i].contains("s") && !text_lines[i].contains("---")) {
float ttc_val = text_lines[i].left(text_lines[i].length() - 1).toFloat();
if (ttc_val < 3.0f) {
text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - urgent
} else if (ttc_val < 6.0f) {
text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution
} else {
text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White - safe
}
}
else {
text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White for other lines
}
// Draw main text
painter.setPen(text_color);
painter.drawText(textRect, Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]);
}
}
// Reset pen
painter.setPen(Qt::NoPen);
}
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &vd, const QRect &surface_rect) {
const float speedBuff = 10.;

View File

@@ -34,20 +34,11 @@ protected:
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
QPolygonF *pvd, int max_idx, bool allow_invert = true);
void drawLeadStatus(QPainter &painter, int height, int width);
void drawLeadStatusAtPosition(QPainter &painter,
const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos,
int height, int width,
const QString &label);
void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect);
void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line);
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
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);
@@ -64,9 +55,4 @@ protected:
QPointF lead_vertices[2] = {};
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
QRectF clip_region;
float lead_status_alpha = 0.0f;
QPointF lead_status_pos;
QString lead_status_text;
QColor lead_status_color;
};

View File

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

View File

@@ -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 &param = "");
};

View File

@@ -92,6 +92,16 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
// ignore events when device is awakened by resetInteractiveTimeout
ignore = !device()->isAwake();
device()->resetInteractiveTimeout();
#ifdef SUNNYPILOT
auto *s_sp = uiStateSP();
bool onroadScreenControl = s_sp->scene.onroadScreenOffControl;
bool started = s_sp->scene.started;
bool timerExpired = (s_sp->scene.onroadScreenOffTimer == 0);
ignore |= (onroadScreenControl and started and timerExpired);
s_sp->reset_onroad_sleep_timer();
#endif
break;
}
default:

View File

@@ -7,6 +7,10 @@
#include "selfdrive/ui/qt/offroad/onboarding.h"
#include "selfdrive/ui/qt/offroad/settings.h"
#ifdef SUNNYPILOT
#include "selfdrive/ui/sunnypilot/ui.h"
#endif
class MainWindow : public QWidget {
Q_OBJECT

View File

@@ -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):

View File

@@ -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",

View File

@@ -56,14 +56,13 @@ DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(pare
addItem(errorLogBtn);
QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanelSP::updateToggles);
is_release = params.getBool("IsReleaseBranch");
is_tested = params.getBool("IsTestedBranch");
is_development = params.getBool("IsDevelopmentBranch");
}
void DeveloperPanelSP::updateToggles(bool offroad) {
bool disable_updates = params.getBool("DisableUpdates");
bool is_release = params.getBool("IsReleaseBranch") || params.getBool("IsReleaseSpBranch");
bool is_tested = params.getBool("IsTestedBranch");
bool is_development = params.getBool("IsDevelopmentBranch");
prebuiltToggle->setVisible(!is_release && !is_tested && !is_development);
prebuiltToggle->setEnabled(disable_updates);
@@ -80,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) {

View File

@@ -22,9 +22,6 @@ private:
ParamControlSP *prebuiltToggle;
Params params;
ParamControlSP *showAdvancedControls;
bool is_development;
bool is_release;
bool is_tested;
private slots:
void updateToggles(bool offroad);

View File

@@ -88,7 +88,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
connect(toggleDeviceBootMode, &ButtonParamControlSP::buttonClicked, this, [=](int index) {
params.put("DeviceBootMode", QString::number(index).toStdString());
updateState();
updateState(offroad);
});
addItem(device_grid_layout);
@@ -122,7 +122,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
addItem(power_group_layout);
std::vector always_enabled_btns = {
always_enabled_btns = {
rebootBtn,
poweroffBtn,
offroadBtn,
@@ -130,17 +130,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
buttons["onroadUploadsBtn"],
};
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool _offroad) {
for (auto btn : findChildren<PushButtonSP*>()) {
bool always_enabled = std::find(always_enabled_btns.begin(), always_enabled_btns.end(), btn) != always_enabled_btns.end();
if (!always_enabled) {
btn->setEnabled(offroad);
}
}
offroad = _offroad;
updateState();
});
QObject::connect(uiState(), &UIState::offroadTransition, this, &DevicePanelSP::updateState);
}
void DevicePanelSP::setOffroadMode() {
@@ -164,7 +154,7 @@ void DevicePanelSP::setOffroadMode() {
ConfirmationDialog::alert(tr("Disengage to Enter Always Offroad Mode"), this);
}
updateState();
updateState(offroad);
}
void DevicePanelSP::resetSettings() {
@@ -181,12 +171,16 @@ void DevicePanelSP::resetSettings() {
}
void DevicePanelSP::showEvent(QShowEvent *event) {
updateState();
updateState(offroad);
}
void DevicePanelSP::updateState() {
if (!isVisible()) {
return;
void DevicePanelSP::updateState(bool _offroad) {
for (auto btn : findChildren<PushButtonSP*>()) {
bool always_enabled = std::find(always_enabled_btns.begin(), always_enabled_btns.end(), btn) != always_enabled_btns.end();
if (!always_enabled) {
btn->setEnabled(_offroad);
}
}
bool offroad_mode_param = params.getBool("OffroadMode");
@@ -204,4 +198,6 @@ void DevicePanelSP::updateState() {
} else {
AddWidgetAt(0, offroadBtn);
}
offroad = _offroad;
}

View File

@@ -23,7 +23,7 @@ public:
explicit DevicePanelSP(SettingsWindowSP *parent = 0);
void showEvent(QShowEvent *event) override;
void setOffroadMode();
void updateState();
void updateState(bool _offroad);
void resetSettings();
private:
@@ -34,6 +34,8 @@ private:
QVBoxLayout *power_group_layout;
bool offroad;
std::vector<PushButtonSP*> always_enabled_btns = {};
const QString alwaysOffroadStyle = R"(
PushButtonSP {
border-radius: 20px;

View File

@@ -29,7 +29,7 @@ OnroadScreenBrightnessControl::OnroadScreenBrightnessControl(const QString &para
"Onroad Brightness",
"",
"",
{0, 100}, 10, true);
{0, 90}, 10, true);
connect(onroadScreenOffTimer, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);
connect(onroadScreenBrightness, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);

View File

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

View File

@@ -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",

View File

@@ -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();
}
@@ -158,3 +174,7 @@ void SpeedLimitSettings::refresh() {
void SpeedLimitSettings::showEvent(QShowEvent *event) {
refresh();
}
void SpeedLimitSettings::hideEvent(QHideEvent *event) {
setCurrentWidget(subPanelFrame);
}

View File

@@ -21,6 +21,7 @@ public:
SpeedLimitSettings(QWidget *parent = nullptr);
void refresh();
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
signals:
void backPress();
@@ -34,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");

View File

@@ -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"),
@@ -86,7 +99,13 @@ void LongitudinalPanel::showEvent(QShowEvent *event) {
refresh(offroad);
}
void LongitudinalPanel::hideEvent(QHideEvent *event) {
main_layout->setCurrentWidget(cruisePanelScreen);
}
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()) {
@@ -99,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.");
@@ -115,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;
}

View File

@@ -18,13 +18,14 @@ class LongitudinalPanel : public QWidget {
public:
explicit LongitudinalPanel(QWidget *parent = nullptr);
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void refresh(bool _offroad);
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;
@@ -34,6 +35,7 @@ private:
ParamControl *SmartCruiseControlVision;
ParamControl *SmartCruiseControlMap;
ParamControl *intelligentCruiseButtonManagement = nullptr;
ParamControl *dynamicExperimentalControl = nullptr;
SpeedLimitSettings *speedLimitScreen;
PushButtonSP *speedLimitSettings;
};

View File

@@ -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()) {

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
paramsRefresh(param_name, param_value);
});
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
is_sunnylink_enabled = params.getBool("SunnylinkEnabled");
connect(uiStateSP(), &UIStateSP::sunnylinkRolesChanged, this, &SunnylinkPanel::updatePanel);
connect(uiStateSP(), &UIStateSP::sunnylinkDeviceUsersChanged, this, &SunnylinkPanel::updatePanel);
connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) {
@@ -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);
@@ -272,7 +272,7 @@ void SunnylinkPanel::updatePanel() {
const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A"));
sunnylinkEnabledBtn->setEnabled(!is_onroad);
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
is_sunnylink_enabled = params.getBool("SunnylinkEnabled");
bool is_sub = uiStateSP()->isSunnylinkSponsor() && is_sunnylink_enabled;
auto max_current_sponsor_rule = uiStateSP()->sunnylinkSponsorRole();
auto role_name = max_current_sponsor_rule.getSponsorTierString();
@@ -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("");

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,5 +22,5 @@ public:
void updateSettings() override;
private:
bool offroad = false;
ParamControlSP *coopSteeringToggle = nullptr;
};

View File

@@ -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);
@@ -140,6 +140,40 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
vlayout->addWidget(sunnypilotScroller);
main_layout->addWidget(sunnypilotScreen);
QObject::connect(uiState(), &UIState::offroadTransition, this, &VisualsPanel::refreshLongitudinalStatus);
refreshLongitudinalStatus();
}
void VisualsPanel::refreshLongitudinalStatus() {
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>();
has_longitudinal_control = hasLongitudinalControl(CP);
} else {
has_longitudinal_control = false;
}
if (chevron_info_settings) {
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);
} else {
// Reset to "Off" when longitudinal not available
params.put("ChevronInfo", "0");
chevron_info_settings->setDescription(chevronNoLongDescription);
}
// Enable only when longitudinal is available
chevron_info_settings->setEnabled(has_longitudinal_control);
chevron_info_settings->refresh();
}
}
void VisualsPanel::paramsRefresh() {

View File

@@ -19,6 +19,7 @@ public:
explicit VisualsPanel(QWidget *parent = nullptr);
void paramsRefresh();
void refreshLongitudinalStatus();
protected:
QStackedLayout* main_layout = nullptr;
@@ -29,4 +30,6 @@ protected:
ParamWatcher * param_watcher;
ButtonParamControlSP *chevron_info_settings;
ButtonParamControlSP *dev_ui_settings;
bool has_longitudinal_control = false;
};

View File

@@ -18,4 +18,10 @@ void AnnotatedCameraWidgetSP::updateState(const UIState &s) {
void AnnotatedCameraWidgetSP::showEvent(QShowEvent *event) {
AnnotatedCameraWidget::showEvent(event);
ui_update_params_sp(uiState());
uiStateSP()->reset_onroad_sleep_timer(OnroadTimerStatusToggle::RESUME);
}
void AnnotatedCameraWidgetSP::hideEvent(QHideEvent *event) {
AnnotatedCameraWidget::hideEvent(event);
uiStateSP()->reset_onroad_sleep_timer(OnroadTimerStatusToggle::PAUSE);
}

View File

@@ -18,4 +18,5 @@ public:
protected:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent* event) override;
};

View File

@@ -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,45 +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();
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();
@@ -125,7 +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;
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) {
@@ -218,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();
@@ -228,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);
}
@@ -250,6 +278,7 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
// No Alerts displayed
else {
e2eAlertFrame = 0;
if (!isStandstill) standstillElapsedTime = 0.0;
}
// Blinker
@@ -540,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;
}
@@ -634,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);
@@ -683,10 +713,23 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
}
}
// Draw "MAX" text
p.setFont(InterFont(40, QFont::DemiBold));
// Draw "MAX" or carState.cruiseState.speedCluster (when ICBM is active) text
if (!pcmCruiseSpeed && carControlEnabled) {
if (std::nearbyint(set_speed) != std::nearbyint(speedCluster)) {
icbm_active_counter = 3 * UI_FREQ;
} else if (icbm_active_counter > 0) {
icbm_active_counter--;
}
} else {
icbm_active_counter = 0;
}
int max_str_size = (icbm_active_counter != 0) ? 60 : 40;
int max_str_y = (icbm_active_counter != 0) ? 15 : 27;
QString max_str = (icbm_active_counter != 0) ? QString::number(std::nearbyint(speedCluster)) : tr("MAX");
p.setFont(InterFont(max_str_size, QFont::DemiBold));
p.setPen(max_color);
p.drawText(set_speed_rect.adjusted(0, 27, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("MAX"));
p.drawText(set_speed_rect.adjusted(0, max_str_y, 0, 0), Qt::AlignTop | Qt::AlignHCenter, max_str);
// Draw set speed
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(set_speed)) : "";
@@ -696,27 +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;
int y = surface_rect.center().y() + 40;
x += devUiInfo > 0 ? 0 : 50;
y += devUiInfo > 0 ? 0 : 80;
QRect alertRect(x - size, y - size, size * 2, size * 2);
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;
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);
@@ -733,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));
@@ -761,19 +803,33 @@ 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 || rightBlinkerOn) ? 1 : 0;
if (!leftBlinkerOn && !rightBlinkerOn) {
blinkerFrameCounter = 0;
lastBlinkerStatus = 0;
return;
}
if (blinkerStatus != lastBlinkerStatus) {
blinkerFrameCounter = 0;
lastBlinkerStatus = blinkerStatus;
}
++blinkerFrameCounter;
const int circleRadius = 44;
const int arrowLength = 44;
const int x_gap = 180;
const int BLINKER_COOLDOWN_FRAMES = UI_FREQ / 10;
if (blinkerFrameCounter < BLINKER_COOLDOWN_FRAMES) {
return;
}
const int circleRadius = 60;
const int arrowLength = 60;
const int x_gap = 160;
const int y_offset = 272;
const int centerX = surface_rect.center().x();
const bool hazard = leftBlinkerOn && rightBlinkerOn;
const QPen bgBorder(Qt::white, 5);
const QPen arrowPen(Qt::NoPen);

View File

@@ -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;
@@ -114,5 +113,11 @@ private:
bool leftBlindspot;
bool rightBlindspot;
int blinkerFrameCounter;
int lastBlinkerStatus;
bool showTurnSignals;
bool carControlEnabled;
float speedCluster = 0;
int icbm_active_counter = 0;
bool pcmCruiseSpeed = true;
};

View File

@@ -21,70 +21,234 @@ 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 = Params().getBool("BlindSpot");
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 = Params().getBool("RainbowMode");
//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) {
auto *s = uiState();
auto &sm = *(s->sm);
bool longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl();
if (!longitudinal_control) {
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
return;
}
if (!sm.alive("radarState")) {
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
return;
}
const auto &radar_state = sm["radarState"].getRadarState();
const auto &lead_one = radar_state.getLeadOne();
const auto &lead_two = radar_state.getLeadTwo();
bool has_lead_one = lead_one.getStatus();
bool has_lead_two = lead_two.getStatus();
if (!has_lead_one && !has_lead_two) {
lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f);
if (lead_status_alpha <= 0.0f) return;
} else {
lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f);
}
if (has_lead_one) {
drawLeadStatusPosition(painter, lead_one, lead_vertices[0], height, width);
}
if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) {
drawLeadStatusPosition(painter, lead_two, lead_vertices[1], height, width);
}
}
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();
auto &sm = *(s->sm);
float v_ego = sm["carState"].getCarState().getVEgo();
int chevron_data = s->scene.chevron_info;
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35;
QFont content_font = painter.font();
content_font.setPixelSize(50);
content_font.setBold(true);
painter.setFont(content_font);
bool is_metric = s->scene.is_metric;
QStringList text_lines;
const int chevron_all = 4;
QStringList chevron_text[3];
// Distance display
if (chevron_data == 1 || chevron_data == chevron_all) {
int pos = 0;
float val = std::max(0.0f, d_rel);
QString unit = is_metric ? "m" : "ft";
if (!is_metric) val *= 3.28084f;
chevron_text[pos].append(QString::number(val, 'f', 0) + " " + unit);
}
// Speed display
if (chevron_data == 2 || chevron_data == chevron_all) {
int pos = (chevron_data == 2) ? 0 : 1;
float multiplier = is_metric ? static_cast<float>(MS_TO_KPH) : static_cast<float>(MS_TO_MPH);
float val = std::max(0.0f, (v_rel + v_ego) * multiplier);
QString unit = is_metric ? "km/h" : "mph";
chevron_text[pos].append(QString::number(val, 'f', 0) + " " + unit);
}
// Time to contact
if (chevron_data == 3 || chevron_data == chevron_all) {
int pos = (chevron_data == 3) ? 0 : 2;
float val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f;
QString ttc = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---";
chevron_text[pos].append(ttc);
}
for (int i = 0; i < 3; ++i) {
if (!chevron_text[i].isEmpty()) text_lines.append(chevron_text[i]);
}
if (text_lines.isEmpty()) return;
QFontMetrics fm(content_font);
float text_width = 120.0f;
for (const QString &line : text_lines) {
text_width = std::max(text_width, fm.horizontalAdvance(line) + 20.0f);
}
text_width = std::min(text_width, 250.0f);
float line_height = 50.0f;
float total_height = text_lines.size() * line_height;
float margin = 20.0f;
float text_y = chevron_pos.y() + sz + 15;
if (text_y + total_height > height - margin) {
float y_max = chevron_pos.y() > (height - margin) ? (height - margin) : chevron_pos.y();
text_y = y_max - 15 - total_height;
text_y = std::max(margin, text_y);
}
float text_x = chevron_pos.x() - text_width / 2;
text_x = std::clamp(text_x, margin, (float)width - text_width - margin);
QPoint shadow_offset(2, 2);
QColor text_color = QColor(255, 255, 255, (int)(255 * lead_status_alpha));
for (int i = 0; i < text_lines.size(); ++i) {
float y = text_y + (i * line_height);
if (y + line_height > height - margin) break;
QRect rect(text_x, y, text_width, line_height);
// Draw shadow
painter.setPen(QColor(0, 0, 0, (int)(200 * lead_status_alpha)));
painter.drawText(rect.translated(shadow_offset), Qt::AlignCenter, text_lines[i]);
painter.setPen(text_color);
painter.drawText(rect, Qt::AlignCenter, text_lines[i]);
}
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);
}

View File

@@ -13,10 +13,19 @@ 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;
void drawLeadStatus(QPainter &painter, int height, int width);
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;
// Lead status animation
float lead_status_alpha = 0.0f;
};

View File

@@ -25,10 +25,8 @@ void OnroadWindowSP::updateState(const UIStateSP &s) {
void OnroadWindowSP::mousePressEvent(QMouseEvent *e) {
OnroadWindow::mousePressEvent(e);
uiStateSP()->reset_onroad_sleep_timer();
}
void OnroadWindowSP::offroadTransition(bool offroad) {
OnroadWindow::offroadTransition(offroad);
uiStateSP()->reset_onroad_sleep_timer();
}

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