mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-10 15:54:49 +08:00
Compare commits
84 Commits
mazda-butt
...
feature/sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
525f95c1d2 | ||
|
|
80f285a6c0 | ||
|
|
d2f0a09511 | ||
|
|
9ad5e80451 | ||
|
|
063aa994d2 | ||
|
|
50462a1d01 | ||
|
|
437726b348 | ||
|
|
9e6af5ba74 | ||
|
|
258daaec7f | ||
|
|
99bd9075d5 | ||
|
|
c438aeb5a5 | ||
|
|
f1ca81debf | ||
|
|
fd0aa9feb8 | ||
|
|
d7e1c42c2b | ||
|
|
6d51d64285 | ||
|
|
e0ccc175e4 | ||
|
|
734151f59b | ||
|
|
9a14baac4d | ||
|
|
d3e3628a95 | ||
|
|
fec6382b96 | ||
|
|
4bd020e92b | ||
|
|
7f5342f378 | ||
|
|
339bc0b8b3 | ||
|
|
59c64acc29 | ||
|
|
7229c7541e | ||
|
|
39e73cc46e | ||
|
|
285fd97606 | ||
|
|
e5f1f86ac2 | ||
|
|
7e03277962 | ||
|
|
a315d8e03c | ||
|
|
bd9bb74d03 | ||
|
|
1462814dcf | ||
|
|
7d54b58b8d | ||
|
|
2d9ceb728a | ||
|
|
68d059fd5d | ||
|
|
728108f97f | ||
|
|
cb6fa622ee | ||
|
|
5b29fd0f2c | ||
|
|
6f42bbab18 | ||
|
|
b89393a5a2 | ||
|
|
1e5758e712 | ||
|
|
974a7a3f7d | ||
|
|
fe6edda23a | ||
|
|
17e25f78b4 | ||
|
|
9b92cdd2cc | ||
|
|
d6317ffd20 | ||
|
|
3ba52bc6fe | ||
|
|
d7fd78050b | ||
|
|
8864b79a6e | ||
|
|
e9f054b7ee | ||
|
|
f429f3191f | ||
|
|
3df1b53fab | ||
|
|
6bb87174b9 | ||
|
|
6d356d520e | ||
|
|
73123aa400 | ||
|
|
7dfe03b7a3 | ||
|
|
12a4b1b561 | ||
|
|
7aac14e6fc | ||
|
|
ae21d40a19 | ||
|
|
41abede7f6 | ||
|
|
f653566803 | ||
|
|
e8a39c4a74 | ||
|
|
517020ffb6 | ||
|
|
a85f3ce11c | ||
|
|
014baf8e90 | ||
|
|
8050c56a43 | ||
|
|
0b826002e9 | ||
|
|
408d52d72a | ||
|
|
6786ffd274 | ||
|
|
5162a66bc8 | ||
|
|
7a73be03da | ||
|
|
3469b71fbe | ||
|
|
8b62b6b65b | ||
|
|
0cd165a6be | ||
|
|
8c14d43572 | ||
|
|
689529956d | ||
|
|
fdc4734efe | ||
|
|
6f346c97ca | ||
|
|
aeaac22274 | ||
|
|
f28cea759d | ||
|
|
de64b99740 | ||
|
|
0e1de37281 | ||
|
|
5a309daee6 | ||
|
|
520649b893 |
46
.github/workflows/forum-docs.yml
vendored
Normal file
46
.github/workflows/forum-docs.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Publish Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: 'Set to true to publish to forum'
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
DOCS_CATEGORY_ID: 114
|
||||
DOCS_DATA_EXPLORER_QUERY_ID: 2
|
||||
DOCS_TARGET: https://forum.sunnypilot.ai
|
||||
DOCS_API_KEY: ${{ secrets.DISCOURSE_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
|
||||
jobs:
|
||||
# dry_run:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: ruby/setup-ruby@v1
|
||||
# with:
|
||||
# ruby-version: "3.3"
|
||||
# bundler-cache: true
|
||||
# working-directory: ./release/ci
|
||||
# - run: bundle exec ./sync_docs.rb --dry-run
|
||||
# working-directory: ./release/ci
|
||||
|
||||
publish:
|
||||
# if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true')
|
||||
# needs: dry_run
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: "3.3"
|
||||
bundler-cache: true
|
||||
working-directory: ./release/ci
|
||||
- run: bundle exec ./sync_docs.rb
|
||||
working-directory: ./release/ci
|
||||
@@ -118,8 +118,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:100) {
|
||||
nodes {
|
||||
... on PullRequest {
|
||||
number
|
||||
@@ -149,7 +149,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:${PR_LABEL},${PR_LABEL}-c3 draft:false sort:created-asc")
|
||||
|
||||
PR_LIST=${PR_LIST//\'/}
|
||||
echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@ a.out
|
||||
.cache/
|
||||
|
||||
/docs_site/
|
||||
/docs_sp_site/
|
||||
|
||||
*.mp4
|
||||
*.dylib
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||
{"HideVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
@@ -159,14 +160,17 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"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"}},
|
||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// MADS params
|
||||
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
@@ -193,6 +197,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SunnylinkDongleId", {PERSISTENT, STRING}},
|
||||
{"SunnylinkdPid", {PERSISTENT, INT}},
|
||||
{"SunnylinkEnabled", {PERSISTENT, BOOL, "1"}},
|
||||
{"SunnylinkTempFault", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0"}},
|
||||
|
||||
// Backup Manager params
|
||||
{"BackupManager_CreateBackup", {PERSISTENT, BOOL}},
|
||||
@@ -200,6 +205,8 @@ 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"}},
|
||||
|
||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
@@ -242,4 +249,13 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"MapTargetVelocities", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
|
||||
{"SmartCruiseControlMap", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SmartCruiseControlVision", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// Torque lateral control custom params
|
||||
{"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}},
|
||||
{"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}},
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
# 334 Supported Cars
|
||||
# 337 Supported Cars
|
||||
|
||||
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|Setup Video|
|
||||
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
@@ -83,7 +83,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Civic Hatchback Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Civic Hybrid 2025">Buy Here</a></sub></details>|||
|
||||
|Honda|Clarity 2018-21|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector + Honda Clarity Proxy Board<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://shop.retropilot.org/product/honda-clarity-proxy-board-kit">Buy Here</a></sub></details>|||
|
||||
@@ -99,7 +99,9 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|HR-V 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda HR-V 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Insight 2019-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Insight 2019-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Inspire 2018|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Inspire 2018">Buy Here</a></sub></details>|||
|
||||
|Honda|N-Box 2018|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|11 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda N-Box 2018">Buy Here</a></sub></details>|||
|
||||
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec 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=Honda Odyssey 2018-20">Buy Here</a></sub></details>|||
|
||||
|Honda|Odyssey 2021-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|43 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch 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=Honda Odyssey 2021-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec 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=Honda Passport 2019-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec 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=Honda Pilot 2016-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2023-25">Buy Here</a></sub></details>|||
|
||||
@@ -163,6 +165,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|EV6 (without HDA II) 2022-24[<sup>6</sup>](#footnotes)|Highway Driving Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L 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=Kia EV6 (without HDA II) 2022-24">Buy Here</a></sub></details>|||
|
||||
|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G 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=Kia Forte 2019-21">Buy Here</a></sub></details>|||
|
||||
|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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=Kia Forte 2022-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Forte Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G 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=Kia Forte Non-SCC 2019">Buy Here</a></sub></details>|||
|
||||
|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia K5 2021-24">Buy Here</a></sub></details>|||
|
||||
|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia K5 Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Kia|K8 Hybrid (with HDA II) 2023[<sup>6</sup>](#footnotes)|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q 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=Kia K8 Hybrid (with HDA II) 2023">Buy Here</a></sub></details>|||
|
||||
@@ -259,7 +262,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Tesla A connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<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=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Tesla B connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<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=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Tesla A connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<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=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model Y (with HW4) 2024[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Tesla B connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<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=Tesla Model Y (with HW4) 2024">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Tesla B connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<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=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard 2019-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard Hybrid 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2016">Buy Here</a></sub></details>|||
|
||||
|
||||
36
docs_sp/SAFETY.md
Normal file
36
docs_sp/SAFETY.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Safety
|
||||
|
||||
openpilot is an Adaptive Cruise Control (ACC) and Automated Lane Centering (ALC) system.
|
||||
Like other ACC and ALC systems, openpilot is a failsafe passive system and it requires the
|
||||
driver to be alert and to pay attention at all times.
|
||||
|
||||
In order to enforce driver alertness, openpilot includes a driver monitoring feature
|
||||
that alerts the driver when distracted.
|
||||
|
||||
However, even with an attentive driver, we must make further efforts for the system to be
|
||||
safe. We repeat, **driver alertness is necessary, but not sufficient, for openpilot to be
|
||||
used safely** and openpilot is provided with no warranty of fitness for any purpose.
|
||||
|
||||
openpilot is developed in good faith to be compliant with FMVSS requirements and to follow
|
||||
industry standards of safety for Level 2 Driver Assistance Systems. In particular, we observe
|
||||
ISO26262 guidelines, including those from [pertinent documents](https://www.nhtsa.gov/sites/nhtsa.dot.gov/files/documents/13498a_812_573_alcsystemreport.pdf)
|
||||
released by NHTSA. In addition, we impose strict coding guidelines (like [MISRA C : 2012](https://www.misra.org.uk/what-is-misra/))
|
||||
on parts of openpilot that are safety relevant. We also perform software-in-the-loop,
|
||||
hardware-in-the-loop and in-vehicle tests before each software release.
|
||||
|
||||
Following Hazard and Risk Analysis and FMEA, at a very high level, we have designed openpilot
|
||||
ensuring two main safety requirements.
|
||||
|
||||
1. The driver must always be capable to immediately retake manual control of the vehicle,
|
||||
by stepping on the brake pedal or by pressing the cancel button.
|
||||
2. The vehicle must not alter its trajectory too quickly for the driver to safely
|
||||
react. This means that while the system is engaged, the actuators are constrained
|
||||
to operate within reasonable limits[^1].
|
||||
|
||||
For additional safety implementation details, refer to [panda safety model](https://github.com/commaai/panda#safety-model). For vehicle specific implementation of the safety concept, refer to [panda/board/safety/](https://github.com/commaai/panda/tree/master/board/safety).
|
||||
|
||||
**Extra note**: comma.ai strongly discourages the use of openpilot forks with safety code either missing or
|
||||
not fully meeting the above requirements.
|
||||
|
||||
[^1]: For these actuator limits we observe ISO11270 and ISO15622. Lateral limits described there translate to 0.9 seconds of maximum actuation to achieve a 1m lateral deviation.
|
||||
|
||||
3
docs_sp/assets/sp_logo.svg
Normal file
3
docs_sp/assets/sp_logo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:675116a9b50eb713b2266396aff1460eed8263738455b1d49365f48168b4d4f9
|
||||
size 1698
|
||||
13
docs_sp/branches/definitions.md
Normal file
13
docs_sp/branches/definitions.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Definitions
|
||||
|
||||
| Branch | Definition | Supported Devices | Description | Stability/Readiness |
|
||||
|:--------------:|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
|
||||
| `release` | Release branch | Comma 3X | Stable release branch. After testing on `staging`, updates are pushed here and published publicly. | **Ready to Use:** Highly stable, recommended for most users. |
|
||||
| `staging` | Staging branch | Comma 3X | Pre-release testing branch. Community feedback is essential to identify issues before public release. | **Varied Stability:** Generally stable, but intended for testing before public release. |
|
||||
| `dev` | Development branches | Comma 3X | Experimental branch with the latest features and bug fixes brought in manually. Expect bugs and braking changes. | **Experimental:** Least stable, suitable for testers and developers. |
|
||||
| `master` | Primary development branch | Comma 3X | All Pull Requests are merged here for future releases. CI automatically strips, minifies, and pushes changes to `staging`. Running the `master` branch is suitable for development purposes but not recommended for non-development use. | **For Development Use:** Suitable for developers, may be unstable for general use. |
|
||||
| `release-tici` | Release branch | Comma 3 | Stable release branch. After testing on `staging-tici`, updates are pushed here and published publicly. | **Ready to Use:** Highly stable, recommended for most users. |
|
||||
| `staging-tici` | Staging branch | Comma 3 | Pre-release testing branch. Community feedback is essential to identify issues before public release. | **Varied Stability:** Generally stable, but intended for testing before public release. |
|
||||
|
||||
!!! tip
|
||||
Your feedback is invaluable. Testers, even without software development experience, are encourage to run `dev` or `staging` and report issues.
|
||||
17
docs_sp/branches/recommended-branches.md
Normal file
17
docs_sp/branches/recommended-branches.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Recommended Branches
|
||||
|
||||
=== "Comma 3X"
|
||||
|
||||
| Branch | Installation URL | Change Logs |
|
||||
|:---------:|----------------------------------- |---------------------------------------------------------------------------------|
|
||||
| `release` | **Coming Soon** | **Coming Soon** |
|
||||
| `staging` | [install.sunnypilot.ai/staging]() | [CHANGELOG](https://github.com/sunnyhaibin/sunnypilot/blob/staging/RELEASES.md) |
|
||||
| `dev` | [install.sunnypilot.ai/dev]() | [CHANGELOG](https://github.com/sunnyhaibin/sunnypilot/blob/dev/RELEASES.md) |
|
||||
|
||||
=== "Comma 3"
|
||||
|
||||
| Branch | Installation URL | Change Logs |
|
||||
|:---------:|--------------------------------------- |---------------------------------------------------------------------------------|
|
||||
| `release-tici` | **Coming Soon** | **Coming Soon** |
|
||||
| `staging-tici` | [install.sunnypilot.ai/staging-tici]() | [CHANGELOG](https://github.com/sunnyhaibin/sunnypilot/blob/staging/RELEASES.md) |
|
||||
|
||||
68
docs_sp/community/CONTRIBUTING.md
Normal file
68
docs_sp/community/CONTRIBUTING.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# How to contribute
|
||||
|
||||
Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about open-sourcing and externalization](https://www.sunnypilot.ai/blog/july/a-new-chapter-transparency/). Development activity is coordinated through our [GitHub Issues](https://github.com/sunnypilot/sunnypilot/issues), [GitHub Discussions](https://github.com/sunnypilot/sunnypilot/discussions), and [Discord](https://discord.sunnypilot.ai).
|
||||
|
||||
### Getting Started
|
||||
|
||||
* Setup your [development environment](https://github.com/sunnypilot/sunnypilot/tree/master/tools)
|
||||
* Read about the [development workflow](WORKFLOW.md)
|
||||
* Join our [Discord](https://discord.sunnypilot.ai)
|
||||
* Docs are at [https://docs.sunnypilot.ai](https://docs.sunnypilot.ai) and [https://www.sunnypilot.ai/blog](https://www.sunnypilot.ai/blog)
|
||||
|
||||
## What contributions are we looking for?
|
||||
|
||||
**sunnypilot's priorities are [safety](../SAFETY.md), stability, quality, and features, in that order.** Aligning with comma's ideals, part of sunnypilot's mission is to *solve self-driving cars while delivering shippable intermediaries*, and **all** development is towards that goal.
|
||||
|
||||
### What gets merged?
|
||||
|
||||
The probability of a pull request being merged is a function of its value to the project and the effort it will take us to get it merged.
|
||||
If a PR offers *some* value but will take lots of time to get merged, it will be closed.
|
||||
Simple, well-tested bug fixes are the easiest to merge, and new features are the hardest to get merged.
|
||||
|
||||
All of these are examples of good PRs:
|
||||
|
||||
* [typo fix](https://github.com/commaai/openpilot/pull/30678)
|
||||
* [removing unused code](https://github.com/commaai/openpilot/pull/30573)
|
||||
* [simple car model port](https://github.com/commaai/openpilot/pull/30245)
|
||||
* [car brand port](https://github.com/commaai/openpilot/pull/23331)
|
||||
* [UI design changes](https://github.com/sunnypilot/sunnypilot/commit/84f6fce90639135611ec568c4d39a352a300bede)
|
||||
* [new features](https://github.com/sunnypilot/sunnypilot/commit/68e1379003bfdb599921cf9cd5684bfb762fd676)
|
||||
|
||||
### What doesn't get merged?
|
||||
|
||||
* **arbitrary style changes**: code is art, and it's up to the author to make it beautiful
|
||||
* **500+ line PRs**: clean it up, break it up into smaller PRs, or both
|
||||
* **PRs without a clear goal**: every PR must have a singular and clear goal
|
||||
|
||||
### First contribution
|
||||
|
||||
Check out any [good first issue from commaai's openpilot](https://github.com/commaai/openpilot/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to get started.
|
||||
|
||||
### What do I need to contribute?
|
||||
|
||||
A lot of sunnypilot work requires only a PC, and some requires a comma device.
|
||||
Most car-related contributions require access to that car, plus a comma device installed in the car.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Pull requests should be against the [`master`](https://github.com/sunnypilot/sunnypilot) branch. If you're unsure about a contribution, feel free to open a discussion, issue, or draft PR to discuss the problem you're trying to solve.
|
||||
|
||||
A good pull request has all of the following:
|
||||
|
||||
- a clearly stated purpose
|
||||
- every line changed directly contributes to the stated purpose
|
||||
- verification, i.e. how did you test your PR?
|
||||
- justification
|
||||
|
||||
* if you've optimized something, post benchmarks to prove it's better
|
||||
* if you've improved your car's tuning, post before and after plots
|
||||
|
||||
- passes the CI tests
|
||||
|
||||
## Contributing without Code
|
||||
|
||||
* Report bugs in [GitHub issues](https://github.com/sunnypilot/sunnypilot/issues).
|
||||
* Report driving issues in the `#general` Discord channel.
|
||||
* Consider opting into driver camera uploads to improve the driver monitoring model.
|
||||
* Connect your device to Wi-Fi regularly, so that comma can pull data for training better driving models.
|
||||
* Run the `staging-c3` branch and report issues. This branch is like `master` but it's built just like a release.
|
||||
43
docs_sp/community/WORKFLOW.md
Normal file
43
docs_sp/community/WORKFLOW.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# sunnypilot development workflow
|
||||
|
||||
Aside from the ML models, most tools used for sunnypilot development are in this repo.
|
||||
|
||||
Most development happens on normal Ubuntu workstations, and not in cars or directly on comma devices. See the [setup guide](https://github.com/sunnypilot/sunnypilot/tree/master/tools) for getting your PC setup for sunnypilot development.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# get the latest stuff
|
||||
git pull
|
||||
git lfs pull
|
||||
git submodule update --init --recursive
|
||||
|
||||
# update dependencies
|
||||
tools/ubuntu_setup.sh
|
||||
|
||||
# build everything
|
||||
scons -j$(nproc)
|
||||
|
||||
# build just the ui with either of these
|
||||
scons -j8 selfdrive/ui/
|
||||
cd selfdrive/ui/ && scons -u -j8
|
||||
|
||||
# test everything
|
||||
pytest
|
||||
|
||||
# test just logging services
|
||||
cd system/loggerd && pytest .
|
||||
|
||||
# run the linter
|
||||
op lint
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Automated Testing
|
||||
|
||||
All PRs and commits are automatically checked by GitHub Actions. Check out `.github/workflows/` for what GitHub Actions runs. Any new tests should be added to GitHub Actions.
|
||||
|
||||
### Code Style and Linting
|
||||
|
||||
Code is automatically checked for style by GitHub Actions as part of the automated tests. You can also run these tests yourself by running `pre-commit run --all`.
|
||||
274
docs_sp/community/reporting-a-bug.md
Normal file
274
docs_sp/community/reporting-a-bug.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Bug Reports
|
||||
|
||||
sunnypilot is an actively maintained project that we constantly strive to improve. With project of this size and complexity,
|
||||
bugs may occur. If you think you have discovered a bug, you can help us by submitting an issue
|
||||
in [comma's public issue tracker][comma's issue tracker],
|
||||
[sunnypilot's public issue tracker][sunnypilot's issue tracker] or on our [Discord][discord], following this guide.
|
||||
|
||||
[comma's issue tracker]: https://github.com/commaai/openpilot/issues
|
||||
[sunnypilot's issue tracker]: https://github.com/sunnypilot/sunnypilot/issues
|
||||
[discord]: https://discord.sunnypilot.ai
|
||||
|
||||
## Before creating an issue
|
||||
|
||||
With more than 2,500 users, issues are created frequently. The maintainers of this project are trying very hard to keep
|
||||
the number of open issues and reports down by fixing bugs as fast as possible. By following this guide, you will know
|
||||
exactly what information we need to help you quickly.
|
||||
|
||||
**But first, please do the following things before creating an issue.**
|
||||
|
||||
### Upgrade to the latest version
|
||||
|
||||
Chances are that the bug you discovered was already fixed in a subsequent version. Thus, before reporting an issue,
|
||||
ensure that you're running the [latest release version](https://github.com/sunnypilot/sunnypilot/releases) of sunnypilot.
|
||||
Please consult our [installation guide](../setup/read-before-installing.md) to learn how to upgrade to the latest version.
|
||||
|
||||
!!! warning "Bug fixes are not backported"
|
||||
Please understand that only bugs that occur in the latest version of sunnypilot will be addressed. Also, to reduce
|
||||
duplicate efforts, fixes cannot be backported to earlier versions.
|
||||
|
||||
### Remove customizations
|
||||
|
||||
If you're using customized features, such as your own tweaks of the features, please remove them from the branch
|
||||
you are testing from before reporting a bug. We can't offer official support for bugs that might hide in your implementations,
|
||||
so make sure to omit any customizations from the version being tested.
|
||||
|
||||
If, after removing the customizations, the bug is gone, the bug is likely caused by your customizations. A good idea is
|
||||
to add them back gradually to narrow down the root cause of the problem If you did a major version upgrade, make sure
|
||||
you adjusted all customizations you have implemented.
|
||||
|
||||
!!! tip
|
||||
If you are an advanced user, you could also utilize `git bisect`
|
||||
to perform a binary search in the history to find a particular regression.
|
||||
|
||||
!!! warning "Customizations mentioned in our documentation"
|
||||
A handful of the features sunnypilot offers can only be implemented with customizations. if you find a bug in any of
|
||||
the customizations that our documentations explicitly mentioned, you are, of course, encouraged to report it.
|
||||
|
||||
**Don't be shy to ask on our [Discord][discord] for help if you run into problems.**
|
||||
|
||||
### Search for solutions
|
||||
|
||||
At this stage, we know that the problem persists in the latest version and is not caused by any of your customizations.
|
||||
However, the problem might result from a small typo or a syntactical error in the source code, e.g., `selfdrive/car/interfaces.py`.
|
||||
|
||||
Now, before you go through the trouble of creating a bug report that is answered and closed right away with a link to
|
||||
the relevant documentation section or another already reported or closed issue or discussion, you can save time for us
|
||||
and yourself by doing some research:
|
||||
|
||||
1. [Search our documentation] and look for the relevant sections that could be related to your problem. If found, make
|
||||
sure that the settings are configured correctly.
|
||||
2. [Search our Discord][discord] to learn if other users are struggling with similar problems and work together with
|
||||
our
|
||||
great community towards a solution. Many problems are solved there.
|
||||
3. [Search comma's openpilot issue tracker][comma's issue tracker], as another user might
|
||||
already have reported the same problem that may exist in
|
||||
stock openpilot, and there might even be a known workaround or fix for it. Thus, no need to create a new issue.
|
||||
4. [Search sunnypilot's issue tracker][sunnypilot's issue tracker], as another user might already have
|
||||
reported the same problem, and there
|
||||
might even be a known workaround or fix for it. Thus, no need to create a new issue.
|
||||
|
||||
[Search our documentation]: ?q=
|
||||
|
||||
**Keep track of all <u>search terms</u> and <u>relevant links</u>, you'll need them in the bug report.**[^1]
|
||||
|
||||
[^1]:
|
||||
We might be using terminology in our documentation different from yours, but we mean the same. When you include the
|
||||
search terms and related links in your bug report, you help us to adjust and improve the documentation.
|
||||
|
||||
---
|
||||
|
||||
At this point, when you still haven't found a solution to your problem, we encourage you to report the issue on our
|
||||
[Discord][discord] because it's now very likely that you stumbled over something we don't know
|
||||
yet. Read the following section
|
||||
to learn how to create a complete and helpful bug report.
|
||||
|
||||
## Issue template
|
||||
|
||||
We have created an issue template to make the bug reporting process as simple as possible, and more efficient for our
|
||||
community and us.
|
||||
|
||||
- [Title]
|
||||
- [Context]<small>optional</small>
|
||||
- [Bug description]
|
||||
- [Related links]
|
||||
- [Reproduction]
|
||||
- [Steps to reproduce]
|
||||
- [Checklist]
|
||||
|
||||
[Title]: #title
|
||||
[Context]: #context
|
||||
[Bug description]: #bug-description
|
||||
[Related links]: #related-links
|
||||
[Reproduction]: #reproduction
|
||||
[Steps to reproduce]: #steps-to-reproduce
|
||||
[Checklist]: #checklist
|
||||
|
||||
### Title
|
||||
|
||||
A good title is short and descriptive. It should be a one-sentence executive summary of the issue, so the impact and
|
||||
severity of the bug you want to report can be inferred from the title.
|
||||
|
||||
| <!-- --> | Example |
|
||||
| -------- |--------------------------------------------------------------------------------------------------------------|
|
||||
| :material-check:{ style="color: #4DB6AC" } __Clear__ | Speed Limit Control (SLC) stuck in `preActive` when engaged |
|
||||
| :material-close:{ style="color: #EF5350" } __Wordy__ | The Speed Limit Control (SLC) remains in the `preActive` state when longitudinal it's supposed to be engaged |
|
||||
| :material-close:{ style="color: #EF5350" } __Unclear__ | SLC does not work |
|
||||
| :material-close:{ style="color: #EF5350" } __Useless__ | Help |
|
||||
|
||||
### Context <small>optional</small> { #context }
|
||||
|
||||
Before describing the bug, you can provide additional context for us to understand what you were trying to achieve.
|
||||
Explain the circumstances in which you're using sunnypilot, and what you _think_ might be relevant. Don't write
|
||||
about the bug here.
|
||||
|
||||
!!! note "__Why this might be helpful__"
|
||||
Some errors only manifest in specific settings, environments or edge cases, for example, when the feature is not available
|
||||
to certain cars.
|
||||
|
||||
### Bug description
|
||||
|
||||
Now, to the bug you want to report. Provide a clear, focused, specific, and concise summary of the bug you encountered.
|
||||
Explain why you think this is a bug that should be reported to sunnypilot, and not to one of its dependencies.[^3]
|
||||
Adhere to the following principles:
|
||||
|
||||
[^3]:
|
||||
Sometimes, users report bugs on our [sunnypilot's issue tracker] or [Discord][discord]
|
||||
that are caused by one of our upstream dependencies, including [comma's openpilot], [comma's panda],
|
||||
or other openpilot forks' dependencies. A good rule of thumb is
|
||||
to reproduce the issue with stock openpilot in the same conditions and
|
||||
check if the problem persists. If it does, the problem is likely not
|
||||
related to sunnypilot and should be reported upstream. When in
|
||||
doubt, use our [Discord][discord] to ask for help.
|
||||
|
||||
[comma's openpilot]: https://github.com/commaai/openpilot
|
||||
[comma's panda]: https://github.com/commaai/panda
|
||||
|
||||
- __Explain the <u>what</u>, not the <u>how</u>__ – don't explain
|
||||
[how to reproduce the bug][Steps to reproduce] here, we're getting there.
|
||||
Focus on articulating the problem and its impact as clearly as possible.
|
||||
|
||||
- __Keep it short and concise__ – if the bug can be precisely explained in one
|
||||
or two sentences, perfect. Don't inflate it – maintainers and future users
|
||||
will be grateful for having to read less.
|
||||
|
||||
- __One bug at a time__ – if you encounter several unrelated bugs, please
|
||||
create separate issues for them. Don't report them in the same issue, as
|
||||
this makes attribution difficult.
|
||||
|
||||
---
|
||||
|
||||
:material-run-fast: __Stretch goal__ – if you found a workaround or a way to fix
|
||||
the bug, you can help other users temporarily mitigate the problem before
|
||||
we maintainers can fix the bug in our code base.
|
||||
|
||||
!!! note "__Why we need this__"
|
||||
In order for us to understand the problem, we need a clear description of it and quantify its impact, which is
|
||||
essential for triage and prioritization.
|
||||
|
||||
### Related links
|
||||
|
||||
Of course, prior to reporting a bug, you have read our documentation and
|
||||
[could not find a working solution][search for solutions]. Please share links
|
||||
to all sections of our documentation that might be relevant to the bug, as it
|
||||
helps us gradually improve it.
|
||||
|
||||
Additionally, since you have searched [comma's issue tracker], [sunnypilot's issue tracker] or [Discord][discord]
|
||||
before reporting an issue, and have possibly found several issues or
|
||||
discussions, include those as well. Every link to an issue or discussion creates
|
||||
a backlink, guiding us maintainers and other users in the future.
|
||||
|
||||
---
|
||||
|
||||
:material-run-fast: __Stretch goal__ – if you also include the search terms you
|
||||
used when [searching for a solution][search for solutions] to your problem, you
|
||||
make it easier for us maintainers to improve the documentation.
|
||||
|
||||
[search for solutions]: #search-for-solutions
|
||||
|
||||
### Reproduction
|
||||
|
||||
A minimal reproduction is at the heart of every well-written bug report, as
|
||||
it allows us maintainers to instantly recreate the necessary conditions to
|
||||
inspect the bug to quickly find its root cause. It's a proven fact that issues
|
||||
with concise and small reproductions can be fixed much faster.
|
||||
|
||||
After you have created the reproduction, take note of your <u>__comma Dongle ID__</u>. It will be used during the bug
|
||||
report.
|
||||
|
||||
!!! note "__Why we need this__"
|
||||
If an issue contains no minimal reproduction or just a link to a repository with thousands of files, the
|
||||
maintainers would need to invest a lot of time into trying to recreate the right conditions to even inspect the
|
||||
bug, let alone fix it.
|
||||
|
||||
!!! warning "Don't share links to repositories"
|
||||
While we know that it is a good practice among developers to include a link
|
||||
to a repository with the bug report, we currently don't support those in our
|
||||
process. The reason is that the reproduction, which is automatically
|
||||
produced by the <u>__route ID__</u> contains all the necessary
|
||||
environment information that is often forgotten to be included.
|
||||
|
||||
Additionally, there are many non-technical users of sunnypilot that
|
||||
have trouble creating repositories.
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
At this point, you provided us with enough information to understand the bug
|
||||
and provided us with a reproduction that we could run and inspect. However, when
|
||||
we check your reproduction, it might not be immediately apparent how we can see
|
||||
the bug in action.
|
||||
|
||||
Thus, please list the specific steps we should follow when running your
|
||||
reproduction to observe the bug. Keep the steps short and concise, and make sure
|
||||
not to leave anything out. Use simple language as you would explain it to a
|
||||
five-year-old, and focus on continuity.
|
||||
|
||||
!!! note "__Why we need this__"
|
||||
We must know how to navigate your reproduction in order
|
||||
to observe the bug, as some bugs only occur at certain viewports or in
|
||||
specific conditions.
|
||||
|
||||
### Uploading logs and preserving routes
|
||||
|
||||
After reproducing the bug, please follow these steps to upload the necessary logs and preserve the routes.
|
||||
|
||||
1. Ensure the route is fully uploaded at [comma Connect]. We cannot look
|
||||
into issues without routes, or at least a comma Dongle ID.
|
||||
|
||||
1. Visit [comma Connect], select the route with the issue reproduced.
|
||||
2. Under the "Files" button, locate "All logs". Click "Upload x files".
|
||||
3. View the upload queue, and confirm that all raw logs are uploaded.
|
||||
|
||||
!!! note
|
||||
Sometimes when the qlogs of the route are still being uploaded, some raw logs may not be available to
|
||||
request for upload. Refresh the page a few times once you have confirmed all qlogs have been uploaded,
|
||||
then try to upload all raw logs again if available.
|
||||
|
||||
2. Share your Dongle ID with sunnypilot on [comma Connect].
|
||||
|
||||
1. Visit [comma Connect], navigate to the gear icon.
|
||||
2. Select "Share by email", and enter `support@sunnypilot.ai`.
|
||||
3. Confirm the sharing by clicking the share icon again.
|
||||
4. Set the device name to your vehicle's year/make/model and your Discord username, so it can be easily identified.
|
||||
|
||||
3. Once all raw logs are uploaded, click "More info" and enable the "Preserved" option to preserve the route.
|
||||
4. Attach the route ID in your issue submission.
|
||||
|
||||
[comma Connect]: https://connect.comma.ai
|
||||
|
||||
### Checklist
|
||||
|
||||
Thanks for following the guide and creating a high-quality and complete bug
|
||||
report – you are almost done. The checklist ensures that you have read this guide
|
||||
and have worked to your best knowledge to provide us with everything we need to
|
||||
know to help you.
|
||||
|
||||
- [ ] I have upgraded to the latest release version of sunnypilot.
|
||||
- [ ] I have removed or disable any customizations and confirmed the bug persists.
|
||||
- [ ] I have searched the documentation, issue trackers, and Discord for similar issues.
|
||||
- [ ] I have created a minimal reproduction and noted my comma Dongle ID.
|
||||
- [ ] I have shared my Dongle ID with sunnypilot at `support@sunnypilot.ai`.
|
||||
- [ ] I have filled out all required sections of the issue template.
|
||||
- [ ] I have followed this guide and ensured all necessary information is included.
|
||||
|
||||
__We'll take it from here.__
|
||||
97
docs_sp/community/reporting-a-docs-issue.md
Normal file
97
docs_sp/community/reporting-a-docs-issue.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Documentation issues
|
||||
|
||||
Our documentation is composed of many pages and includes extensive
|
||||
information on features, configurations, customizations, and much more. If you
|
||||
have found an inconsistency or see room for improvement, please follow this
|
||||
guide to submit an issue on our [issue tracker].
|
||||
|
||||
[issue tracker]: https://github.com/sunnypilot/sunnypilot/issues
|
||||
|
||||
## Issue template
|
||||
|
||||
Reporting a documentation issue is usually less involved than reporting a bug.
|
||||
Please thoroughly read this guide before creating a new documentation issue,
|
||||
and provide the following information as part of the issue:
|
||||
|
||||
- [Title]
|
||||
- [Description]
|
||||
- [Related links]
|
||||
- [Proposed change] <small>optional</small>
|
||||
- [Checklist]
|
||||
|
||||
[Title]: #title
|
||||
[Description]: #description
|
||||
[Related links]: #related-links
|
||||
[Proposed change]: #proposed-change
|
||||
[Checklist]: #checklist
|
||||
|
||||
### Title
|
||||
|
||||
A good title should be a short, one-sentence description of the issue, contain
|
||||
all relevant information and, in particular, keywords to simplify the search in
|
||||
our issue tracker.
|
||||
|
||||
| <!-- --> | Example |
|
||||
| -------- | -------- |
|
||||
| :material-check:{ style="color: #4DB6AC" } __Clear__ | Clarify Speed Limit Control engagement
|
||||
| :material-close:{ style="color: #EF5350" } __Unclear__ | Missing information in the docs
|
||||
| :material-close:{ style="color: #EF5350" } __Useless__ | Help
|
||||
|
||||
### Description
|
||||
|
||||
Provide a clear and concise summary of the inconsistency or issue you
|
||||
encountered in the documentation or the documentation section that needs
|
||||
improvement. Explain why you think the documentation should be adjusted and
|
||||
describe the severity of the issue:
|
||||
|
||||
- __Keep it short and concise__ – if the inconsistency or issue can be
|
||||
precisely explained in one or two sentences, perfect. Maintainers and future
|
||||
users will be grateful for having to read less.
|
||||
|
||||
- __One issue at a time__ – if you encounter several unrelated inconsistencies,
|
||||
please create separate issues for them. Don't report them in the same issue
|
||||
– it makes attribution difficult.
|
||||
|
||||
!!! note "__Why we need this__"
|
||||
Describing the problem clearly and concisely is a prerequisite for improving
|
||||
our documentation – we need to understand what's wrong, so we can fix it.
|
||||
|
||||
### Related links
|
||||
|
||||
After you described the documentation section that needs to be adjusted above,
|
||||
we now ask you to share the link to this specific documentation section and
|
||||
other possibly related sections. Make sure to use anchor links (permanent links)
|
||||
where possible, as it simplifies discovery.
|
||||
|
||||
!!! note "__Why we need this__"
|
||||
Providing the links to the documentation help us understand which sections
|
||||
of our documentation need to be adjusted, extended, or overhauled.
|
||||
|
||||
### Proposed change <small>optional</small> { #proposed-change }
|
||||
|
||||
Now that you have provided us with the description and links to the
|
||||
documentation sections, you can help us, maintainers, and the community by
|
||||
proposing an improvement. You can sketch out rough ideas or write a concrete
|
||||
proposal. This field is optional but very helpful.
|
||||
|
||||
!!! note "__Why we need this__"
|
||||
An improvement proposal can be beneficial for other users who encounter
|
||||
the same issue, as they offer solutions before we maintainers can update
|
||||
the documentation.
|
||||
|
||||
### Checklist
|
||||
|
||||
Thanks for following the guide and providing valuable feedback for our
|
||||
documentation – you are almost done. The checklist ensures that you have read
|
||||
this guide and have worked to your best knowledge to provide us with every piece
|
||||
of information we need to improve it.
|
||||
|
||||
- [ ] I have provided a clear and descriptive title for the documentation issue.
|
||||
- [ ] I have summarized the inconsistency or issue concisely in the description.
|
||||
- [ ] I have included links to the specific documentation section(s) that need
|
||||
adjustments.
|
||||
- [ ] (Optional) I have proposed a change or improvement to the documentation.
|
||||
- [ ] I have followed this guide and ensured all necessary information is included.
|
||||
|
||||
__We'll take it from here.__
|
||||
|
||||
24
docs_sp/features/auto-lane-change.md
Normal file
24
docs_sp/features/auto-lane-change.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Auto Lane Change
|
||||
description: Detailed documentation on the Auto Lane Change feature in sunnypilot.
|
||||
---
|
||||
|
||||
# Auto Lane Change
|
||||
|
||||
sunnypilot's Auto Lane Change feature allows the vehicle to automatically change lanes.
|
||||
|
||||
## Key Setting and How to Use
|
||||
|
||||
- **Nudge to Confirm:** This is the default method. To perform a lane change, activate the turn signal in the desired direction and then gently turn the steering wheel (nudges) in the same direction to confirm the maneuver.
|
||||
- **Nudge-less Lane Change:** For a more automated experience, users can enable a "nudge-less" option. With this setting, you only need to activate the turn signal to initiate a lane-change.
|
||||
- **Nudge-less with Delay** This is same as Nudge-less mode, but with a fixed delay. Once the turn-indicator is activated, sunnypilot will wait for the selected delay before initiating the lane change.
|
||||
|
||||
### Additional Related Settings
|
||||
- **Delay with Blind Spot:** This crucial safety feature adds a delay to the lane change if the vehicle's blind spot monitoring (BSM) system detects an object. The lane change will only proceed after the blind spot is clear.
|
||||
|
||||
## Important Considerations
|
||||
|
||||
- **Manual Override:** The driver can regain control at any time by taking control of the steering wheel.
|
||||
- **Driver Monitoring:** sunnypilot utilizes driver monitoring to ensure the driver remains attentive.
|
||||
- **Object Detection:** The system is primarily designed to follow lane lines and may not detect all objects. The driver must always be prepared to take control.
|
||||
- **Lane Markings:** The feature's performance is dependent on clear and visible lane markings.
|
||||
13
docs_sp/features/custom-acc-increments.md
Normal file
13
docs_sp/features/custom-acc-increments.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Custom ACC Increments
|
||||
description: Detailed documentation on the Custom ACC Increments feature in sunnypilot.
|
||||
---
|
||||
|
||||
# Custom ACC Increments
|
||||
|
||||
sunnypilot offers customization to the set speed increments for Adaptive Cruise Control (ACC) when sunnypilot is controlling the vehicle's longitudinal control. This allows for more precise speed adjustments compared to the default behavior of many vehicles.
|
||||
|
||||
- **Short press**: This controls the speed adjustment when the "RES+" or "SET-" button is pressed for a short period of time.
|
||||
- Allowed values: 1 through 10
|
||||
- **Long press**: This controls the speed adjustment when the "RES+" or "SET-" button is pressed for a longer duration.
|
||||
- Allowed values: 1, 5, 10
|
||||
14
docs_sp/features/dynamic-experimental-control.md
Normal file
14
docs_sp/features/dynamic-experimental-control.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Dynamic Experimental Control
|
||||
description: Detailed documentation on the Dynamic Experimental Control feature in sunnypilot.
|
||||
---
|
||||
|
||||
# Dynamic Experimental Control (DEC)
|
||||
|
||||
Dynamic Experimental Control (DEC) intelligently switches between the standard adaptive cruise control (ACC) and the end-to-end longitudinal control of Experimental Mode based on the driving conditions.
|
||||
|
||||
**Chill Mode** is the standard adaptive cruise control method. It is designed to provide a smooth, predictable driving experience. However, it does not provide advanced driving capabilities like stopping at red lights and stop signs.
|
||||
|
||||
**Experimental Mode** allows the system to control the vehicle's speed and tries to drive like a human, enabling it to slow down for turns, stop at red lights, and stop signs. While capable and experimental, this mode can sometimes be overly cautious, especially on highways.
|
||||
|
||||
***Dynamic Experimental Control*** aims to provide the best of both worlds: the advanced capabilities of Experimental Mode when needed, and the smooth, predictable behavior of the standard system for less complex driving scenarios. This allows for a more natural driving experience by using Experimental Mode in situations where it excels, such as city driving and tight turns, while reverting to the default behavior for highway driving.
|
||||
97
docs_sp/features/hyundai-longitudinal-tuning.md
Normal file
97
docs_sp/features/hyundai-longitudinal-tuning.md
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
# **How custom longitudinal tuning works for Hyundai/Kia/Genesis vehicles in sunnypilot.**
|
||||
|
||||
To begin this documentation, I would like to first present the safety guidelines followed to create the tune:
|
||||
|
||||
Our main safety guideline considered is [ISO 15622:2018](https://www.iso.org/obp/ui/en/#iso:std:iso:15622:ed-3:v1:en)
|
||||
This provides the groundwork of safety limits this tune must adhere too, and therefore, must be followed.
|
||||
For example, in our jerk calculations throughout this tune, you will see how maximum jerk is clipped using the equation provided.
|
||||
|
||||
In the tuning you will see a set of equations, the first being jerk, **but what exactly is jerk?**
|
||||
Jerk is calculated by taking current acceleration (in the form of m/s^2), subtracting that by previous acceleration, and
|
||||
dividing that by time. In our tune you will see the following equation:
|
||||
|
||||
planned_accel = CC.actuators.accel
|
||||
current_accel = CS.out.aEgo
|
||||
blended_value = 0.67 * planned_accel + 0.33 * current_accel
|
||||
delta = blended_value - self.state.accel_last_jerk
|
||||
|
||||
self.state.jerk = math.copysign(delta * delta, delta)
|
||||
self.state.accel_last_jerk = blended_value
|
||||
|
||||
Instead of using a hardcoded time, we are focused on making jerk parabolic. First we have our planned acceleration from longitudinal_planner.
|
||||
Then we have our current carstate acceleration. These are then blended together 67:33 = 100% to form our blended value.
|
||||
Following this, we have our delta which subtracts our blended_value from our previous acceleration `self.state.accel_last_jerk`
|
||||
Lastly, we have our finalized jerk calculation, which squares the delta to create a parabolic response while retaining the original sign,
|
||||
which could be positive or negative (e.g., 5.0 or -5.0). This then goes through our minimum and maximum clipping
|
||||
which forces a value between our set min and max, which I discuss later in this readme.
|
||||
|
||||
Moving on, the accel_last_jerk, stores current accel after each iteration and uses that in the calculation as previous accel for
|
||||
our jerk calculations. Now we see the calculation of jerk max and jerk min.
|
||||
|
||||
### Let's dive into how jerk lower limit max is calculated:
|
||||
|
||||
velocity = CS.out.vEgo
|
||||
if velocity < 5.0:
|
||||
decel_jerk_max = self.car_config.jerk_limits[1]
|
||||
elif velocity > 20.0:
|
||||
decel_jerk_max = 2.5
|
||||
else:
|
||||
decel_jerk_max = 3.64284 - 0.05714 * velocity
|
||||
|
||||
This equation above is set by ISO 15622, and dictates that jerk lower limit can only be five when below 5 m/s. In our equation,
|
||||
|
||||
self.car_config.jerk_limits[1]
|
||||
|
||||
Jerk_limits[1] represents a jerk value of 3.3 m/s^3, which is the maximum analyzed lower jerk rate seen on stock SCC CAN.
|
||||
Between 5 m/s and 20 m/s jerk is capped using the calculation:
|
||||
|
||||
decel_jerk_max = 3.64284 - 0.05714 * velocity
|
||||
|
||||
This equation calculates the linear jerk from 6m/s to 19m/s, scaling down from 3.3 to 2.5 m/s^3.
|
||||
This means that if current velocity is say, 15 m/s the final jerk max value would be capped at 2.78 m/s^3.
|
||||
Anything above 20 m/s is capped to a lower jerk max of 2.5 m/s^3. This allows for a smoother jerk range, while complying to ISO standards to a tee.
|
||||
The current jerk Lower Limit you will see in openpilot before this tune, is 5.0 m/s^3; Which as you can see from using the above calculation,
|
||||
the 5.0 m/s^3 technically does not comply with ISO standards at any speed above 5.0 m/s^3.
|
||||
Having our jerk max be clipped to these values not only allows for better consistency with ISO standards,
|
||||
but also enables us to have a much smoother braking experience.
|
||||
|
||||
### Getting into our next topic, I would like to explain how our minimum jerk was chosen.
|
||||
|
||||
Minimum jerk was chosen based off of the following guideline proposed by Handbook of Intellegent Vehicles (2012):
|
||||
`Ride comfort may be sacrificed only under emergency conditions when vehicle and occupant safety consideration may preclude comfort.`
|
||||
|
||||
### What the value of 0.53 m/s^3 of the jerk lower limit was chosen based off of
|
||||
|
||||
[Carlowitz et al. (2024).](https://www.researchgate.net/publication/382274551_User_evaluation_of_comfortable_deceleration_profiles_for_highly_automated_driving_Findings_from_a_test_track_study)
|
||||
This research study identified the average lower jerk used in comfortable driving settings, which is 0.53 m/s^3.
|
||||
This is then inputted to jerk_limits[0] as 0.53 m/s^3 represents the value used in upper jerk absolute minimum.
|
||||
|
||||
min_lower_jerk = self.car_config.jerk_limits[0]
|
||||
|
||||
As shown above, lower jerk minimum of 0.53 is used for our lower_jerk minimum bounds.
|
||||
|
||||
### Why our minimum upper jerk is conditional
|
||||
|
||||
Our minimum upper band jerk is conditional as well and is denoted below:
|
||||
|
||||
min_upper_jerk = self.car_config.jerk_limits[0] if (velocity > 3.611) else 0.60
|
||||
|
||||
This means that for speeds under 3.611 m/s (8.077 mph/ 13 kph) we have a minimum jerk of 0.60. This allows for smooth
|
||||
takeoffs while not causing lag. For all other speeds, we use our normal jerk_limit for minimum, which is 0.53.
|
||||
|
||||
### Next, we have our acceleration limiting
|
||||
|
||||
For acceleration limiting, we use TCS signal brakeLightsDEPRECATED to measure when to enact the standstill delay
|
||||
which stock SCC uses to allow smoother transitions in acceleration.
|
||||
|
||||
### Lastly, we have our accel value calculations for hyundaican.py
|
||||
|
||||
For our accel value calculations we have the following:
|
||||
|
||||
`self.accel_value = np.clip(self.accel_raw, self.state.accel_last - jerk_number, self.state.accel_last + jerk_number)`
|
||||
|
||||
This essentially means that we have our accel_raw, which is acceleration (m/s^2), followed by our clipping variables.
|
||||
jerk_number in this equation represents exactly `0.1`, which is subtracted or added by self.state.accel_last, which is
|
||||
previous calculated accel_value. Furthermore, we have `self.state.accel_last`, which is calculated as the stored accel from
|
||||
the above calculations.
|
||||
17
docs_sp/features/icbm.md
Normal file
17
docs_sp/features/icbm.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: ICBM (Intelligent Cruise Button Management)
|
||||
description: Detailed documentation on the ICBM feature in sunnypilot.
|
||||
---
|
||||
|
||||
# ICBM (Intelligent Cruise Button Management)
|
||||
|
||||
Intelligent Cruise Button Management (ICBM) is a system designed to manage the vehicle's cruise control set speed by sending cruise control button commands via CAN bus to the car.
|
||||
|
||||
ICBM is particularly useful in vehicles openpilot Longitudinal Control is not available or not desirable to use. By simulating button presses to adjust the stock cruise control set speed, sunnypilot's ICBM can manage the car's speed while keeping native safety features active, such as Forward Collision Warning (FCA) and Automatic Emergency Braking (AEB).
|
||||
|
||||
ICBM also allows the vehicle to use the following features:
|
||||
|
||||
- **[Smart Cruise Control - Vision (SCC-V)]()**
|
||||
- **[Smart Cruise Control - Map (SCC-M)]()**
|
||||
- **[Speed Limit Assist (SLA)]()**
|
||||
- **[Custom ACC Increments]()**
|
||||
17
docs_sp/features/mads.md
Normal file
17
docs_sp/features/mads.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Modular Assistive Driving System (M.A.D.S.)
|
||||
|
||||
Modular Assistive Driving System (MADS) aims to elevate the user's driving experience by modifying the behaviors of
|
||||
driving assist engagements.
|
||||
|
||||
!!! note
|
||||
This feature aligns closely with comma.ai's safety rules.
|
||||
|
||||
## Independent Engagement
|
||||
|
||||
MADS allows users to engage sunnypilot Automatic Lane Centering (ALC) for lateral control and Adaptive Cruise Control
|
||||
(ACC) or Smart Cruise Control (SCC) for longitudinal control independently.
|
||||
|
||||
!!! note "Why This Feature Exists"
|
||||
While newer car models allow for independent engagement of lateral (steering) and longitudinal (speed) control,
|
||||
many older vehicle models and stock openpilot enforce engaging both controls together. MADS introduces this modern
|
||||
convenience to older vehicle models, effectively backporting a feature found in newer cars and providing users more flexibility.
|
||||
15
docs_sp/features/nnlc.md
Normal file
15
docs_sp/features/nnlc.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Neural Network Lateral Control (NNLC)
|
||||
description: Detailed documentation on the NNLC feature in sunnypilot.
|
||||
---
|
||||
|
||||
# Neural Network Lateral Control (NNLC)
|
||||
|
||||
sunnypilot's Neural Network Lateral Control (NNLC) is a feature that enhances the system's ability to steer a vehicle. It enhances the standard lateral controller with one based on a neural network trained on the vehicle's torque data, aiming for smoother and more precise steering adjustments.
|
||||
|
||||
## Key Aspects of NNLC
|
||||
|
||||
- **Improved Accuracy:** The neural network is trained using driving data specific to each vehicle, which allows for more accurate control.
|
||||
- **Smoother Turns:** NNLC inputs past curvature data into its driving model to achieve smoother and more precise lateral control, especially noticeable when taking curves on a highway. Users report that it reduces the oversteering and understeering corrections.
|
||||
|
||||
Formerly known as "NNFF" (Neural Network Feedforward), NNLC aims to make the driving experience feel more natural and less "jittery" in turns.
|
||||
9
docs_sp/features/scc-m.md
Normal file
9
docs_sp/features/scc-m.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Smart Cruise Control - Map (SCC-M)
|
||||
|
||||
Smart Cruise Control - Map (SCC-M) leverages map data to proactively adjust your vehicle's speed by calculating curvature in the road ahead.
|
||||
|
||||
!!! note
|
||||
This feature is only available when sunnypilot is actively controlling the vehicle's longitudinal control.
|
||||
|
||||
!!! note
|
||||
This feature requires OpenStreetMap data for the current location to be downloaded.
|
||||
6
docs_sp/features/scc-v.md
Normal file
6
docs_sp/features/scc-v.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Smart Cruise Control - Vision (SCC-V)
|
||||
|
||||
Smart Cruise Control - Vision (SCC-V) leverages camera vision data to proactively adjust your vehicle's speed by calculating curvature in the road ahead.
|
||||
|
||||
!!! note
|
||||
This feature is only available when sunnypilot is actively controlling the vehicle's longitudinal control.
|
||||
26
docs_sp/features/speed-limit-assist.md
Normal file
26
docs_sp/features/speed-limit-assist.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Speed Limit Assist
|
||||
description: Detailed documentation on the Speed Limit Assist feature in sunnypilot.
|
||||
---
|
||||
|
||||
# Speed Limit Assist (SLA)
|
||||
|
||||
Speed Limit Assist (SLA) is a comprehensive framework in sunnypilot that adjusts the vehicle's set cruise speed according to detected speed limits. It reconciles data from multiple sources to determine the current speed limit.
|
||||
|
||||
## Core Functionality
|
||||
|
||||
SLA utilizes various data sources to determine the speed limit for the current road:
|
||||
|
||||
- **OpenStreetMap (OSM):** Utilizes map data to fetch speed limits. sunnypilot has a refactored OSM implementation for lower resource usage and provides weekly updates.
|
||||
- **Car's Stock System:** On some compatible vehicles, it can pull speed limit data directly from the car's native system.
|
||||
|
||||
## Key Features and Customization
|
||||
|
||||
sunnypilot offers extensive customization for its speed control features:
|
||||
|
||||
- **Selectable Speed Limit Source:** Users can define the priority of data sources (e.g., OSM, Navigation, Vision) for determining the speed limit.
|
||||
- **Configurable Offsets:** You can set an offset above the detected speed limit, either as a fixed value (e.g., +5 mph) or a percentage.
|
||||
- **Engagement Modes:**
|
||||
- **Auto:** Automatically adjusts the cruise speed when a new speed limit is detected.
|
||||
- **User Confirm:** Informs the driver of the new speed limit and waits for them to confirm the change before adjusting the speed.
|
||||
- **Alerts and Warnings:** The system provides alerts to inform the driver before a speed adjustment occurs.
|
||||
9
docs_sp/getting-started/develop-sunnypilot.md
Normal file
9
docs_sp/getting-started/develop-sunnypilot.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# To start developing sunnypilot
|
||||
|
||||
sunnypilot is a fork of [commaai's openpilot](https://github.com/commaai/openpilot), developed by [sunnypilot](https://sunnypilot.ai) and by users like you.
|
||||
We welcome both pull requests and issues on [GitHub](http://github.com/sunnypilot/sunnypilot).
|
||||
|
||||
* Join the [community Discord](https://discord.sunnypilot.ai)
|
||||
* Check out [the contributing docs](../community/CONTRIBUTING.md)
|
||||
* Check out the [openpilot tools](https://github.com/sunnypilot/sunnypilot/tree/master/tools)
|
||||
* Read about the [development workflow](../community/WORKFLOW.md)
|
||||
16
docs_sp/getting-started/use-sunnypilot-in-a-car.md
Normal file
16
docs_sp/getting-started/use-sunnypilot-in-a-car.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# To start using sunnypilot in a car
|
||||
|
||||
To use sunnypilot in a car, you need four things:
|
||||
|
||||
1. **Supported Device:** a comma 3/3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x).
|
||||
|
||||
2. **Software:** The setup procedure for the comma 3/3X allows users to enter a URL for custom software. Use the URL `release-c3.sunnypilot.ai` to install the release version.
|
||||
|
||||
3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CARS.md).
|
||||
|
||||
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3/3X to your car.
|
||||
|
||||
[comma.ai](https://comma.ai) have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup).
|
||||
|
||||
!!! note
|
||||
It's possible to run sunnypilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play.
|
||||
11
docs_sp/getting-started/what-is-sunnypilot.md
Normal file
11
docs_sp/getting-started/what-is-sunnypilot.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# What is sunnypilot?
|
||||
|
||||
sunnypilot is a fork of [comma.ai's openpilot](https://github.com/commaai/openpilot), an open source driver assistance system. sunnypilot offers the user a unique driving experience for over 250+ supported car makes and models with modified behaviors of driving assist engagements. sunnypilot complies with comma.ai's safety rules as accurately as possible.
|
||||
|
||||
## How do I use it?
|
||||
|
||||
sunnypilot is designed to be used on the comma 3/3X.
|
||||
|
||||
## How does it work?
|
||||
|
||||
In short, sunnypilot uses the car's existing APIs for the built-in [ADAS](https://en.wikipedia.org/wiki/Advanced_driver-assistance_system) system and simply provides better acceleration, braking, and steering inputs than the stock system.
|
||||
3
docs_sp/index.md
Normal file
3
docs_sp/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
The documentation here is as best-effort sync from the official https://sunnypilot.github.io/ for ease of access and for AI enhancement when users asks on the forum.
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Prohibited Safety Modifications
|
||||
|
||||
All [official sunnypilot branches](https://github.com/sunnyhaibin/sunnypilot/branches) strictly adhere to [comma.ai's safety policy](https://github.com/commaai/openpilot/blob/master/docs/SAFETY.md). Any changes that go against
|
||||
this policy will result in your fork and your device being banned from both comma.ai and sunnypilot channels.
|
||||
|
||||
The following changes are **VIOLATIONS** of the safety policy and **ARE NOT** supported in any official sunnypilot branches:
|
||||
|
||||
!!! danger "Driver Monitoring"
|
||||
- "Nerfing" or reducing monitoring parameters.
|
||||
|
||||
!!! danger "Panda Safety"
|
||||
- No preventing disengaging of <ins>**longitudinal control**</ins> (positive/negative acceleration) on brake pedal press.
|
||||
- No auto re-engaging of <ins>**longitudinal control**</ins> (positive/negative acceleration) on brake pedal release.
|
||||
- No disengaging on `CRUISE MAIN` in `OFF` state.
|
||||
20
docs_sp/setup/read-before-installing.md
Normal file
20
docs_sp/setup/read-before-installing.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 🚨 Read Before Installing
|
||||
|
||||
It is recommended to read the <u>**entire documentation**</u> before proceeding. This will ensure that you fully understand each added feature in sunnypilot. This also ensures that you are choosing the correct settings and branch for your car to have the best driving experience.
|
||||
|
||||
!!! warning
|
||||
By installing this software, you accept all responsibility for anything that might occur while you use it. sunnypilot and all contributors to sunnypilot are not liable.
|
||||
|
||||
**Use at your own risk.**
|
||||
|
||||
## Installation
|
||||
|
||||
Please refer to the [Recommended Branches](../branches/recommended-branches.md) to find your preferred/supported branch. This guide will assume you want to install the latest `release-c3` branch.
|
||||
|
||||
You can install sunnypilot on your comma 3/3X using one of the following methods:
|
||||
|
||||
- ### [URL Method (Directly on Device)](url-method.md)
|
||||
This method allows you to install sunnypilot directly from your device's screen using a provided URL. It's simple and user-friendly, requiring no additional tools or external devices.
|
||||
|
||||
- ### [SSH Method (Command Line)](ssh-method.md)
|
||||
This method is for advanced users who prefer to use SSH to clone the sunnypilot repository and install it manually via the command line. It offeres greater control over the installation process.
|
||||
28
docs_sp/setup/ssh-method.md
Normal file
28
docs_sp/setup/ssh-method.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# SSH Method
|
||||
|
||||
If you are looking to install sunnypilot via SSH, run the following commands in an SSH terminal after connecting to your comma 3/3X:
|
||||
|
||||
1. Navigate to `data` directory
|
||||
```sh
|
||||
cd /data
|
||||
rm -rf openpilot
|
||||
```
|
||||
|
||||
2. Clone sunnypilot
|
||||
|
||||
!!! example ""
|
||||
`staging` branch is used in this step as an example.
|
||||
```sh
|
||||
git clone -b staging --recurse-submodules https://github.com/sunnypilot/sunnypilot.git openpilot
|
||||
```
|
||||
|
||||
3. Git LFS
|
||||
```sh
|
||||
cd openpilot
|
||||
git lfs pull
|
||||
```
|
||||
|
||||
4. Reboot
|
||||
```sh
|
||||
sudo reboot
|
||||
```
|
||||
18
docs_sp/setup/url-method.md
Normal file
18
docs_sp/setup/url-method.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# URL Method
|
||||
|
||||
The URL installation method can be done in two ways, depending on your device & if you already have sunnypilot installed.
|
||||
|
||||
=== "sunnypilot not installed"
|
||||
|
||||
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, upon reboot, select `Custom Software` when given the option.
|
||||
3. Input the **Installation URL** per [Recommended Branches](../branches/recommended-branches.md).
|
||||
4. Complete the rest of the installation by following the onscreen instructions.
|
||||
|
||||
|
||||
=== "sunnypilot already installed"
|
||||
|
||||
1. On the comma 3/3X, go to `Settings` → `Software`.
|
||||
2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from the sunnypilot repository on GitHub.
|
||||
3. At the `Target Branch` option, press `SELECT` to open the `Target Branch` selector.
|
||||
4. Scroll and select the **Desired Branch** per Recommended Branches.
|
||||
6
docs_sp/stylesheets/style.css
Normal file
6
docs_sp/stylesheets/style.css
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
150
mkdocs-sp.yml
Normal file
150
mkdocs-sp.yml
Normal file
@@ -0,0 +1,150 @@
|
||||
site_name: sunnypilot docs
|
||||
repo_name: sunnypilot/sunnypilot
|
||||
repo_url: https://github.com/sunnypilot/sunnypilot/
|
||||
site_description: sunnypilot Documentation
|
||||
site_url: https://docs.sunnypilot.ai
|
||||
edit_uri: blob/new-docs/docs_sp
|
||||
|
||||
exclude_docs: README.md
|
||||
|
||||
strict: true
|
||||
docs_dir: docs_sp
|
||||
site_dir: docs_sp_site/
|
||||
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
- media: "(prefers-color-scheme)"
|
||||
toggle:
|
||||
icon: material/brightness-auto
|
||||
name: Switch to light mode
|
||||
- scheme: default
|
||||
media: "(prefers-color-scheme: light)"
|
||||
primary: deep purple
|
||||
accent: teal
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
- scheme: slate
|
||||
media: "(prefers-color-scheme: dark)"
|
||||
primary: deep purple
|
||||
accent: teal
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to system preference
|
||||
font:
|
||||
text: Open Sans
|
||||
code: Fira Code
|
||||
logo: assets/sp_logo.svg
|
||||
favicon: assets/sp_logo.svg
|
||||
features:
|
||||
- content.code.copy
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
|
||||
extra_css:
|
||||
- stylesheets/style.css
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- def_list
|
||||
- footnotes
|
||||
- md_in_html
|
||||
- tables
|
||||
- pymdownx.details
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink:
|
||||
normalize_issue_symbols: true
|
||||
repo_url_shorthand: true
|
||||
user: sunnypilot
|
||||
repo: sunnypilot
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
clickable_checkbox: true
|
||||
- toc:
|
||||
permalink: true
|
||||
|
||||
plugins:
|
||||
- git-authors:
|
||||
show_email_address: false
|
||||
- git-committers:
|
||||
repository: sunnypilot/sunnypilot
|
||||
branch: master
|
||||
enabled: !ENV [CI, false]
|
||||
- git-revision-date-localized:
|
||||
enable_creation_date: true
|
||||
- search
|
||||
- redirects:
|
||||
redirect_maps:
|
||||
'index.md': 'getting-started/what-is-sunnypilot.md'
|
||||
|
||||
extra:
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/sunnypilot/sunnypilot
|
||||
- icon: fontawesome/brands/discord
|
||||
link: https://discord.sunnypilot.ai
|
||||
# analytics:
|
||||
# provider: google
|
||||
# property: !ENV GOOGLE_ANALYTICS_KEY
|
||||
# feedback:
|
||||
# title: Was this page helpful?
|
||||
# ratings:
|
||||
# - icon: material/emoticon-happy-outline
|
||||
# name: This page was helpful
|
||||
# data: 1
|
||||
# note: >-
|
||||
# Thanks for your feedback!
|
||||
# - icon: material/emoticon-sad-outline
|
||||
# name: This page could be improved
|
||||
# data: 0
|
||||
# note: >-
|
||||
# Thanks for your feedback! Help us improve this page by
|
||||
# using our <a href="..." target="_blank" rel="noopener">feedback form</a>.
|
||||
|
||||
nav:
|
||||
- Getting Started:
|
||||
- What is sunnypilot?: getting-started/what-is-sunnypilot.md
|
||||
- Use sunnypilot in a car: getting-started/use-sunnypilot-in-a-car.md
|
||||
- Develop sunnypilot: getting-started/develop-sunnypilot.md
|
||||
- Setup:
|
||||
- 🚨 Read before installing 🚨: setup/read-before-installing.md
|
||||
- Installation:
|
||||
- URL Method: setup/url-method.md
|
||||
- SSH Method: setup/ssh-method.md
|
||||
- Features:
|
||||
- Auto Lane Change: features/auto-lane-change.md
|
||||
- Custom ACC Increments: features/custom-acc-increments.md
|
||||
- Dynamic Experimental Control: features/dynamic-experimental-control.md
|
||||
- Hyundai Longitudinal Tuning: features/hyundai-longitudinal-tuning.md
|
||||
- Intelligent Cruise Button Management: features/icbm.md
|
||||
- Modular Assistive Driving System: features/mads.md
|
||||
- Neutral Network Lateral Control: features/nnlc.md
|
||||
- Smart Cruise Control - Map: features/scc-m.md
|
||||
- Smart Cruise Control - Vision: features/scc-v.md
|
||||
- Speed Limit - Assist: features/speed-limit-assist.md
|
||||
- Community:
|
||||
- Contributing: community/CONTRIBUTING.md
|
||||
- Workflow: community/WORKFLOW.md
|
||||
- Reporting a bug: community/reporting-a-bug.md
|
||||
- Reporting a docs issue: community/reporting-a-docs-issue.md
|
||||
- Discord Community: https://discord.sunnypilot.ai
|
||||
- Safety Information:
|
||||
- Safety: SAFETY.md
|
||||
- Prohibited safety modifications: safety-information/prohibited-safety-modifications.md
|
||||
- References:
|
||||
- Branches:
|
||||
- Recommended Branches: branches/recommended-branches.md
|
||||
- Branch Definitions: branches/definitions.md
|
||||
Submodule opendbc_repo updated: bf0aba991a...b8a00bddda
@@ -80,6 +80,13 @@ docs = [
|
||||
"Jinja2",
|
||||
"natsort",
|
||||
"mkdocs",
|
||||
"mkdocs-material",
|
||||
"mkdocs-material-extensions",
|
||||
"mkdocs-git-revision-date-localized-plugin",
|
||||
"mkdocs-git-committers-plugin-2",
|
||||
"mkdocs-git-authors-plugin",
|
||||
"mkdocs-glightbox",
|
||||
"mkdocs-redirects",
|
||||
]
|
||||
|
||||
testing = [
|
||||
|
||||
5
release/ci/Gemfile
Normal file
5
release/ci/Gemfile
Normal file
@@ -0,0 +1,5 @@
|
||||
source "https://rubygems.org"
|
||||
gem "faraday"
|
||||
gem "faraday-retry"
|
||||
gem "faraday-multipart"
|
||||
gem "listen"
|
||||
112
release/ci/lib/api.rb
Normal file
112
release/ci/lib/api.rb
Normal file
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
def self.client
|
||||
@client ||=
|
||||
Faraday.new(url: DOCS_TARGET) do |conn|
|
||||
conn.request :multipart
|
||||
conn.request :url_encoded
|
||||
conn.request :retry,
|
||||
{
|
||||
methods: %i[get post delete put],
|
||||
retry_statuses: [429],
|
||||
max: 3,
|
||||
retry_block: ->(env:, options:, retry_count:, exception:, will_retry_in:) do
|
||||
puts "Rate limited... will retry in #{will_retry_in}s"
|
||||
end,
|
||||
exceptions: [Faraday::TooManyRequestsError]
|
||||
}
|
||||
conn.response :json, content_type: "application/json"
|
||||
conn.response :raise_error
|
||||
conn.adapter Faraday.default_adapter
|
||||
conn.headers["Api-Key"] = DOCS_API_KEY
|
||||
end
|
||||
end
|
||||
|
||||
def self.edit_post(post_id:, raw:, title: nil, category: nil)
|
||||
if dry_run?
|
||||
puts " DRY RUN: skipping PUT /posts/#{post_id}"
|
||||
return
|
||||
end
|
||||
|
||||
params = {
|
||||
post: {
|
||||
raw: raw,
|
||||
edit_reason: "Synced from github.com/discourse/discourse-developer-docs"
|
||||
}
|
||||
}
|
||||
params[:title] = title if title
|
||||
params[:category] = category if category
|
||||
client.put("/posts/#{post_id}", params)
|
||||
end
|
||||
|
||||
def self.create_topic(external_id:, raw:, category:, title:)
|
||||
if dry_run?
|
||||
puts " DRY RUN: skipping POST /posts"
|
||||
return
|
||||
end
|
||||
client.post("/posts", { title: title, raw: raw, external_id: external_id, category: category })
|
||||
end
|
||||
|
||||
def self.trash_topic(topic_id:)
|
||||
if dry_run?
|
||||
puts " DRY RUN: skipping DELETE /t/#{topic_id}.json"
|
||||
return
|
||||
end
|
||||
|
||||
client.delete("/t/#{topic_id}.json")
|
||||
end
|
||||
|
||||
def self.fetch_current_state
|
||||
result =
|
||||
client.post(
|
||||
"/admin/plugins/explorer/queries/#{DATA_EXPLORER_QUERY_ID}/run",
|
||||
{ params: { category_id: CATEGORY_ID.to_s }.to_json }
|
||||
).body
|
||||
|
||||
raise "Data explorer query failed" if result["success"] != true
|
||||
|
||||
if result["columns"] != %w[t_id first_p_id external_id title raw deleted_at is_index_topic]
|
||||
raise "Data explorer query returned unexpected columns: #{result["columns"].inspect}"
|
||||
end
|
||||
|
||||
result["rows"].map do |row|
|
||||
{
|
||||
topic_id: row[0],
|
||||
first_post_id: row[1],
|
||||
external_id: row[2],
|
||||
title: row[3],
|
||||
raw: row[4],
|
||||
deleted_at: row[5],
|
||||
is_index_topic: row[6]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.restore_topic(topic_id:)
|
||||
path = "/t/#{topic_id}/recover.json"
|
||||
|
||||
if dry_run?
|
||||
puts " DRY RUN: skipping PUT #{path}"
|
||||
return
|
||||
end
|
||||
|
||||
client.put(path)
|
||||
end
|
||||
|
||||
def self.upload_file(path)
|
||||
if dry_run?
|
||||
puts " DRY RUN: skipping POST /uploads.json"
|
||||
return { "short_url" => "upload://placeholder" }
|
||||
end
|
||||
|
||||
client.post(
|
||||
"/uploads.json",
|
||||
{ type: "composer", synchronous: true, file: Faraday::UploadIO.new(path, "image/png") }
|
||||
).body
|
||||
end
|
||||
|
||||
def self.dry_run?
|
||||
[nil, true].include?(DRY_RUN)
|
||||
end
|
||||
end
|
||||
108
release/ci/lib/local_doc.rb
Normal file
108
release/ci/lib/local_doc.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Asset = Struct.new(:local_path, :local_sha1, :remote_short_url, keyword_init: true)
|
||||
|
||||
class LocalDoc
|
||||
attr_accessor :path,
|
||||
:frontmatter,
|
||||
:content,
|
||||
:topic_id,
|
||||
:first_post_id,
|
||||
:remote_content,
|
||||
:remote_title,
|
||||
:remote_deleted,
|
||||
:assets
|
||||
|
||||
def initialize(**kwargs)
|
||||
kwargs.each { |k, v| send("#{k}=", v) }
|
||||
self.assets ||= []
|
||||
end
|
||||
|
||||
def external_id
|
||||
"DOC-#{frontmatter["id"]}"
|
||||
end
|
||||
|
||||
def section
|
||||
path_segments = path.split("/")
|
||||
path_segments[0] if path_segments.size > 1
|
||||
end
|
||||
|
||||
def content_with_uploads
|
||||
unused_assets = assets.dup
|
||||
|
||||
result =
|
||||
content.gsub(/![^\]]+\]\(([^)]+)\)/) do |match|
|
||||
path = $1
|
||||
next match if !path.start_with?("/")
|
||||
|
||||
resolved = File.expand_path("#{__dir__}/../#{path}")
|
||||
assets_dir = File.expand_path("#{__dir__}/../assets/")
|
||||
raise "Invalid path: #{resolved}" if !resolved.start_with?(assets_dir)
|
||||
|
||||
digest = Digest::SHA1.file(resolved).hexdigest
|
||||
|
||||
asset = assets.find { |a| a.local_sha1 == digest }
|
||||
unused_assets.delete(asset)
|
||||
if !asset
|
||||
puts " Uploading #{path}..."
|
||||
result = API.upload_file(resolved)
|
||||
raise "File upload failed: #{result.inspect}" if !result["short_url"]
|
||||
asset =
|
||||
Asset.new(local_path: path, local_sha1: digest, remote_short_url: result["short_url"])
|
||||
assets.push(asset)
|
||||
end
|
||||
|
||||
short_url = asset.remote_short_url
|
||||
|
||||
match.gsub(path, short_url)
|
||||
end
|
||||
|
||||
unused_assets.each { |asset| assets.delete(asset) }
|
||||
|
||||
result = <<~MD
|
||||
#{result}
|
||||
|
||||
---
|
||||
|
||||
<small>This document is version controlled - suggest changes [on github](https://github.com/sunnypilot/sunnypilot/blob/master/docs_sp/#{path}).</small>
|
||||
MD
|
||||
|
||||
if assets.size == 0
|
||||
result
|
||||
else
|
||||
<<~MD
|
||||
#{result}
|
||||
<!-- START DOCS ASSET MAP
|
||||
#{serialized_assets}
|
||||
END DOCS ASSET MAP -->
|
||||
MD
|
||||
end
|
||||
end
|
||||
|
||||
def serialized_assets
|
||||
JSON.pretty_generate(
|
||||
assets.map do |asset|
|
||||
{
|
||||
local_path: asset.local_path,
|
||||
local_sha1: asset.local_sha1,
|
||||
remote_short_url: asset.remote_short_url
|
||||
}
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def remote_content=(value)
|
||||
if value.match(/<!-- START DOCS ASSET MAP\n(.+?)\nEND DOCS ASSET MAP -->/m)
|
||||
self.assets = JSON.parse($1).map { |raw_asset| Asset.new(**raw_asset) }
|
||||
end
|
||||
|
||||
value.gsub!(/![^\]]+\]\(([^)]+)\)/) do |match|
|
||||
url = $1
|
||||
found_asset = assets.find { |a| a.remote_short_url == url }
|
||||
match.sub!(path, found_asset.local_path) if found_asset
|
||||
match
|
||||
end
|
||||
|
||||
@remote_content = value
|
||||
end
|
||||
end
|
||||
15
release/ci/lib/util.rb
Normal file
15
release/ci/lib/util.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Util
|
||||
def self.parse_md(raw)
|
||||
if match = raw.match(/\A---\s*\n(.+?)\n---\n?(.*)\z/m)
|
||||
raw_frontmatter, content = match.captures
|
||||
frontmatter = YAML.safe_load(raw_frontmatter)
|
||||
else
|
||||
content = raw
|
||||
frontmatter = {}
|
||||
end
|
||||
|
||||
[frontmatter, content]
|
||||
end
|
||||
end
|
||||
762
release/ci/sync_docs.rb
Executable file
762
release/ci/sync_docs.rb
Executable file
@@ -0,0 +1,762 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "yaml"
|
||||
require "faraday"
|
||||
require "faraday/retry"
|
||||
require "faraday/multipart"
|
||||
require "listen"
|
||||
require "json"
|
||||
require "digest"
|
||||
|
||||
CATEGORY_ID = ENV["DOCS_CATEGORY_ID"].to_i
|
||||
DATA_EXPLORER_QUERY_ID = ENV["DOCS_DATA_EXPLORER_QUERY_ID"].to_i
|
||||
DOCS_TARGET = ENV["DOCS_TARGET"]
|
||||
DOCS_API_KEY = ENV["DOCS_API_KEY"]
|
||||
GEMINI_API_KEY = ENV["GEMINI_API_KEY"]
|
||||
|
||||
VERBOSE = ARGV.include?("-v")
|
||||
WATCH = ARGV.include?("--watch")
|
||||
DRY_RUN = ARGV.include?("--dry-run")
|
||||
|
||||
require_relative "lib/local_doc"
|
||||
require_relative "lib/api"
|
||||
require_relative "lib/util"
|
||||
|
||||
# Gemini API client for title generation
|
||||
class GeminiClient
|
||||
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent"
|
||||
#GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
|
||||
# MAX_REQUESTS_PER_MINUTE = 15
|
||||
MAX_REQUESTS_PER_MINUTE = 9
|
||||
RATE_LIMIT_WINDOW = 60 # seconds
|
||||
|
||||
def initialize(api_key)
|
||||
@api_key = api_key
|
||||
@request_timestamps = []
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
def generate_titles(file_path, content)
|
||||
return nil unless @api_key
|
||||
|
||||
wait_for_rate_limit
|
||||
|
||||
prompt = build_prompt(file_path, content)
|
||||
|
||||
response = Faraday.post(
|
||||
"#{GEMINI_API_URL}?key=#{@api_key}",
|
||||
{ contents: [{ parts: [{ text: prompt }] }] }.to_json,
|
||||
"Content-Type" => "application/json"
|
||||
)
|
||||
|
||||
parse_response(response)
|
||||
rescue => e
|
||||
puts "Error calling Gemini API: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def wait_for_rate_limit
|
||||
@mutex.synchronize do
|
||||
now = Time.now
|
||||
|
||||
# Remove timestamps older than the rate limit window
|
||||
@request_timestamps.reject! { |ts| now - ts > RATE_LIMIT_WINDOW }
|
||||
|
||||
# If we've hit the limit, wait until the oldest request expires
|
||||
if @request_timestamps.length >= MAX_REQUESTS_PER_MINUTE
|
||||
oldest_request = @request_timestamps.first
|
||||
sleep_time = RATE_LIMIT_WINDOW - (now - oldest_request) + 0.1 # Add small buffer
|
||||
|
||||
if sleep_time > 0
|
||||
puts "Rate limit reached (#{MAX_REQUESTS_PER_MINUTE}/min). Waiting #{sleep_time.round(1)}s..."
|
||||
sleep(sleep_time)
|
||||
|
||||
# Clean up again after waiting
|
||||
now = Time.now
|
||||
@request_timestamps.reject! { |ts| now - ts > RATE_LIMIT_WINDOW }
|
||||
end
|
||||
end
|
||||
|
||||
# Record this request
|
||||
@request_timestamps << Time.now
|
||||
end
|
||||
end
|
||||
|
||||
def build_prompt(file_path, content)
|
||||
<<~PROMPT
|
||||
You are helping to generate documentation metadata. Given a markdown file path and its content, generate appropriate titles.
|
||||
|
||||
File path: #{file_path}
|
||||
|
||||
Content preview:
|
||||
#{content[0..500]}
|
||||
|
||||
Please analyze the file path and content, then provide:
|
||||
1. A full title (3-8 words, descriptive and professional, MUST be at least 15 characters long)
|
||||
2. A short title (1-3 words, concise, MUST be at least 15 characters long)
|
||||
|
||||
CRITICAL: Both titles MUST be at least 15 characters long. If a title would be shorter, expand it with relevant context.
|
||||
|
||||
Respond ONLY with valid JSON in this exact format:
|
||||
{
|
||||
"title": "Your Full Title Here",
|
||||
"short_title": "Short Title Here"
|
||||
}
|
||||
|
||||
Do not include any other text, explanation, or markdown formatting.
|
||||
PROMPT
|
||||
end
|
||||
|
||||
def parse_response(response)
|
||||
body = JSON.parse(response.body)
|
||||
text = body.dig("candidates", 0, "content", "parts", 0, "text")
|
||||
|
||||
return nil unless text
|
||||
|
||||
# Extract JSON from potential markdown code blocks
|
||||
json_text = text.strip.gsub(/^```json\n/, "").gsub(/\n```$/, "").strip
|
||||
|
||||
parsed = JSON.parse(json_text)
|
||||
|
||||
# Validate minimum length
|
||||
if parsed["title"] && parsed["title"].length < 15
|
||||
parsed["title"] = parsed["title"].ljust(15)
|
||||
end
|
||||
|
||||
if parsed["short_title"] && parsed["short_title"].length < 15
|
||||
parsed["short_title"] = parsed["short_title"].ljust(15)
|
||||
end
|
||||
|
||||
parsed
|
||||
rescue => e
|
||||
puts "Error parsing Gemini response: #{e.message}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Convert MkDocs "=== Tabs" sections to Obsidian callouts
|
||||
def convert_mkdocs_tabs_to_callouts(content, debug: false)
|
||||
lines = content.lines
|
||||
result = []
|
||||
i = 0
|
||||
match_count = 0
|
||||
|
||||
while i < lines.length
|
||||
line = lines[i]
|
||||
|
||||
# Detect a MkDocs tab start, e.g., === "sunnypilot not installed"
|
||||
if line =~ /^===\s+"([^"]+)"\s*$/
|
||||
match_count += 1
|
||||
tab_title = $1.strip
|
||||
|
||||
# Collect all indented lines following the tab
|
||||
body_lines = []
|
||||
i += 1
|
||||
while i < lines.length
|
||||
current_line = lines[i]
|
||||
|
||||
# Check if line is indented (4+ spaces or tab)
|
||||
if current_line =~ /^(?: |\t)/
|
||||
body_lines << current_line
|
||||
i += 1
|
||||
# Check if it's a blank line - peek ahead like we do for callouts
|
||||
elsif current_line =~ /^\s*$/
|
||||
peek_index = i + 1
|
||||
has_more_indented = false
|
||||
while peek_index < lines.length
|
||||
if lines[peek_index] =~ /^(?: |\t)/
|
||||
has_more_indented = true
|
||||
break
|
||||
elsif lines[peek_index] =~ /^\s*$/
|
||||
peek_index += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if has_more_indented
|
||||
body_lines << current_line
|
||||
i += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
else
|
||||
# Non-indented, non-blank line (could be next tab!) - stop
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
puts "DEBUG: Tab '#{tab_title}', lines: #{body_lines.length}" if debug
|
||||
|
||||
# Convert the tab section to a callout
|
||||
result << "> [!note] #{tab_title}\n"
|
||||
body_lines.each do |body_line|
|
||||
# Remove the first level of indentation (4 spaces or 1 tab) and add > prefix
|
||||
stripped = body_line.sub(/^(?: |\t)/, '')
|
||||
if stripped.strip.empty?
|
||||
result << ">\n"
|
||||
else
|
||||
result << ">#{stripped}"
|
||||
end
|
||||
end
|
||||
else
|
||||
result << line
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
puts "DEBUG: Total tab matches: #{match_count}" if debug
|
||||
result.join
|
||||
end
|
||||
|
||||
|
||||
# New implementation of MkDocs Material callout to Obsidian converter
|
||||
def convert_mkdocs_to_obsidian_callouts(content, debug: false)
|
||||
# Map of MkDocs callout types to Obsidian equivalents
|
||||
callout_map = {
|
||||
'note' => 'note',
|
||||
'tip' => 'tip',
|
||||
'important' => 'important',
|
||||
'warning' => 'warning',
|
||||
'caution' => 'caution',
|
||||
'info' => 'info',
|
||||
'success' => 'success',
|
||||
'question' => 'question',
|
||||
'failure' => 'failure',
|
||||
'danger' => 'danger',
|
||||
'bug' => 'bug',
|
||||
'example' => 'example',
|
||||
'quote' => 'quote',
|
||||
'abstract' => 'abstract',
|
||||
'summary' => 'summary',
|
||||
'tldr' => 'tldr'
|
||||
}
|
||||
|
||||
lines = content.lines
|
||||
result = []
|
||||
i = 0
|
||||
match_count = 0
|
||||
|
||||
while i < lines.length
|
||||
line = lines[i]
|
||||
|
||||
# Check if this line starts a callout (can be indented or not)
|
||||
# Capture any leading indentation
|
||||
if line =~ /^(\s*)!!!\s+(#{callout_map.keys.map { |k| Regexp.escape(k) }.join('|')})(?:\s+"([^"]*)")?\s*$/
|
||||
leading_indent = $1
|
||||
mkdocs_type = $2
|
||||
custom_title = $3
|
||||
match_count += 1
|
||||
obsidian_type = callout_map[mkdocs_type]
|
||||
|
||||
# Determine the base indentation level (number of spaces/tabs before !!!)
|
||||
base_indent_size = leading_indent.length
|
||||
|
||||
# Collect all lines that have MORE indentation than the base
|
||||
body_lines = []
|
||||
i += 1
|
||||
while i < lines.length
|
||||
current_line = lines[i]
|
||||
|
||||
# Check if line has the required base indentation plus more (for body content)
|
||||
# Body content should be indented relative to the !!! line
|
||||
required_indent = leading_indent + " " # base + 4 more spaces
|
||||
alt_required_indent = leading_indent + "\t" # base + 1 tab
|
||||
|
||||
if current_line.start_with?(required_indent) || current_line.start_with?(alt_required_indent)
|
||||
body_lines << current_line
|
||||
i += 1
|
||||
# Check if line is blank - might be between paragraphs
|
||||
elsif current_line =~ /^\s*$/
|
||||
peek_index = i + 1
|
||||
has_more_indented = false
|
||||
while peek_index < lines.length
|
||||
peek_line = lines[peek_index]
|
||||
if peek_line.start_with?(required_indent) || peek_line.start_with?(alt_required_indent)
|
||||
has_more_indented = true
|
||||
break
|
||||
elsif peek_line =~ /^\s*$/
|
||||
peek_index += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if has_more_indented
|
||||
body_lines << current_line
|
||||
i += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
else
|
||||
# Line doesn't have required indentation - stop
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
puts "DEBUG: Match #{match_count} - indent: #{base_indent_size} spaces, type: #{mkdocs_type}, title: #{custom_title.inspect}, body lines: #{body_lines.length}" if debug
|
||||
|
||||
# Build the converted callout, preserving the base indentation
|
||||
if body_lines.empty?
|
||||
if custom_title && !custom_title.empty?
|
||||
result << "#{leading_indent}> [!#{obsidian_type}] \"#{custom_title}\"\n"
|
||||
else
|
||||
result << "#{leading_indent}> [!#{obsidian_type}]\n"
|
||||
end
|
||||
else
|
||||
if custom_title && !custom_title.empty?
|
||||
result << "#{leading_indent}> [!#{obsidian_type}] \"#{custom_title}\"\n"
|
||||
else
|
||||
result << "#{leading_indent}> [!#{obsidian_type}]\n"
|
||||
end
|
||||
# Add > prefix to each body line, removing the extra level of indentation
|
||||
body_lines.each do |body_line|
|
||||
# Remove the base indent + one level (4 spaces or tab)
|
||||
stripped = body_line.sub(/^#{Regexp.escape(leading_indent)}(?: |\t)/, '')
|
||||
result << "#{leading_indent}>#{stripped}"
|
||||
end
|
||||
end
|
||||
else
|
||||
result << line
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
puts "DEBUG: Total matches found: #{match_count}" if debug
|
||||
|
||||
result.join
|
||||
end
|
||||
|
||||
# Convert MkDocs Material icons to standard emojis
|
||||
def convert_material_icons_to_emojis(content)
|
||||
# Map of common Material icons to emoji equivalents
|
||||
icon_map = {
|
||||
# Check/success icons
|
||||
':material-check:' => '✅',
|
||||
':material-check-circle:' => '✅',
|
||||
':material-check-bold:' => '✅',
|
||||
|
||||
# Close/error icons
|
||||
':material-close:' => '❌',
|
||||
':material-close-circle:' => '❌',
|
||||
':material-alert-circle:' => '⚠️',
|
||||
|
||||
# Info icons
|
||||
':material-information:' => 'ℹ️',
|
||||
':material-information-outline:' => 'ℹ️',
|
||||
':material-help-circle:' => '❓',
|
||||
|
||||
# Arrow icons
|
||||
':material-arrow-right:' => '→',
|
||||
':material-arrow-left:' => '←',
|
||||
':material-arrow-up:' => '↑',
|
||||
':material-arrow-down:' => '↓',
|
||||
|
||||
# Other common icons
|
||||
':material-lightbulb:' => '💡',
|
||||
':material-star:' => '⭐',
|
||||
':material-heart:' => '❤️',
|
||||
':material-fire:' => '🔥',
|
||||
':material-flag:' => '🚩',
|
||||
':material-link:' => '🔗',
|
||||
':material-pencil:' => '✏️',
|
||||
':material-delete:' => '🗑️',
|
||||
':material-calendar:' => '📅',
|
||||
':material-clock:' => '🕐',
|
||||
':material-email:' => '📧',
|
||||
':material-phone:' => '📞',
|
||||
}
|
||||
|
||||
# Replace material icons with emojis, ignoring any style attributes
|
||||
icon_map.each do |material_icon, emoji|
|
||||
# Match the icon with optional style attributes like { style="color: #EF5350" }
|
||||
content.gsub!(/#{Regexp.escape(material_icon)}\{\s*style="[^"]*"\s*\}/, emoji)
|
||||
# Also match without style attributes
|
||||
content.gsub!(material_icon, emoji)
|
||||
end
|
||||
|
||||
content
|
||||
end
|
||||
|
||||
# Helper method to generate frontmatter from file path
|
||||
def generate_frontmatter_from_path(path, content = nil, gemini_client = nil)
|
||||
# Remove .md extension and get the base name
|
||||
base_name = File.basename(path, ".md")
|
||||
|
||||
# Generate id from the full path (without extension)
|
||||
# Replace / with -
|
||||
# IMPORTANT: The LocalDoc#external_id method adds "DOC-" prefix (4 chars)
|
||||
# So we need to limit the base ID to 46 chars to stay under the 50 char API limit
|
||||
full_id = path.sub(/\.md$/, "").gsub("/", "-")
|
||||
|
||||
# Maximum length for the base ID (50 char API limit - 4 char "DOC-" prefix)
|
||||
max_base_id_length = 46
|
||||
|
||||
if full_id.length > max_base_id_length
|
||||
# Take first 37 chars and append an 8-char hash for uniqueness (37 + 1 dash + 8 = 46)
|
||||
hash_suffix = Digest::MD5.hexdigest(path)[0..7]
|
||||
id = "#{full_id[0..36]}-#{hash_suffix}"
|
||||
else
|
||||
id = full_id
|
||||
end
|
||||
|
||||
# Try to use Gemini for title generation
|
||||
if gemini_client && content
|
||||
gemini_titles = gemini_client.generate_titles(path, content)
|
||||
|
||||
if gemini_titles
|
||||
return {
|
||||
"id" => id,
|
||||
"title" => gemini_titles["title"],
|
||||
"short_title" => gemini_titles["short_title"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Fallback to original logic if Gemini fails or is not available
|
||||
title = base_name.split(/[-_]/).map(&:capitalize).join(" ")
|
||||
short_title = base_name.split(/[-_]/).map(&:capitalize).join(" ")
|
||||
|
||||
# Ensure minimum length
|
||||
title = title.ljust(15) if title.length < 15
|
||||
short_title = short_title.ljust(15) if short_title.length < 15
|
||||
|
||||
{
|
||||
"id" => id,
|
||||
"title" => title,
|
||||
"short_title" => short_title
|
||||
}
|
||||
end
|
||||
|
||||
# Helper method to generate index.md content for a folder
|
||||
def generate_folder_index(folder_name)
|
||||
# Convert folder name to a nice title (e.g., "my-folder" -> "My Folder")
|
||||
title = folder_name.split(/[-_]/).map(&:capitalize).join(" ")
|
||||
|
||||
"---\ntitle: #{title}\n---\n"
|
||||
end
|
||||
|
||||
# Convert internal markdown links (.md) to Discourse topic links
|
||||
def rewrite_internal_links(content, docs)
|
||||
require "uri"
|
||||
|
||||
content.gsub(/\]\(([^)]+\.md)(#[^)]+)?\)/) do |match|
|
||||
raw_link = $1
|
||||
anchor = $2 || ""
|
||||
# Strip any ./ or ../ from the beginning, but preserve subfolders
|
||||
normalized = raw_link.gsub(%r{^\./}, "").gsub(%r{^\.\./}, "")
|
||||
# Remove trailing .md
|
||||
normalized = normalized.gsub(/\.md$/, "")
|
||||
|
||||
# Try percent-decoding (handles %20 etc)
|
||||
begin
|
||||
normalized_decoded = URI.decode_www_form_component(normalized)
|
||||
rescue
|
||||
normalized_decoded = normalized
|
||||
end
|
||||
|
||||
candidates = []
|
||||
|
||||
# Strategy 1: exact match against doc.path without .md
|
||||
candidates += docs.select { |d| d.path.sub(/\.md$/, "") == normalized_decoded }
|
||||
|
||||
# Strategy 2: ends_with (useful if docs have a different root)
|
||||
if candidates.empty?
|
||||
candidates += docs.select { |d| d.path.end_with?("#{normalized_decoded}.md") }
|
||||
end
|
||||
|
||||
# Strategy 3: match by basename (e.g., linking to index.md or same-named file in subfolder)
|
||||
if candidates.empty?
|
||||
basename = File.basename(normalized_decoded)
|
||||
candidates += docs.select { |d| File.basename(d.path, ".md") == basename }
|
||||
end
|
||||
|
||||
# Strategy 4: index.md handling — if link pointed to a folder/index.md, allow folder match
|
||||
if candidates.empty? && normalized_decoded.end_with?("/index")
|
||||
folder = normalized_decoded.sub(/\/index$/, "")
|
||||
candidates += docs.select { |d| File.dirname(d.path) == folder && File.basename(d.path, ".md") == "index" }
|
||||
end
|
||||
|
||||
# Pick the best candidate (prefer exact match)
|
||||
target_doc =
|
||||
if candidates.any?
|
||||
# prefer exact equality if present
|
||||
exact = candidates.find { |d| d.path.sub(/\.md$/, "") == normalized_decoded }
|
||||
exact || candidates.first
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
if target_doc && target_doc.topic_id
|
||||
# Return a Discourse link preserving the anchor
|
||||
"](/t/-/#{target_doc.topic_id}?silent=true#{anchor})"
|
||||
else
|
||||
if VERBOSE
|
||||
puts "⚠️ rewrite_internal_links: unresolved '#{raw_link}' -> normalized='#{normalized_decoded}'"
|
||||
# show up to 10 possible docs to help debugging
|
||||
sample = docs.first(10).map(&:path).join(", ")
|
||||
puts " sample docs: #{sample}"
|
||||
end
|
||||
# Return original match unchanged so the link doesn't become invalid text
|
||||
match
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Initialize Gemini client if API key is available
|
||||
gemini_client = GEMINI_API_KEY ? GeminiClient.new(GEMINI_API_KEY) : nil
|
||||
|
||||
if gemini_client
|
||||
puts "✓ Gemini API configured for title generation"
|
||||
else
|
||||
puts "⚠ GEMINI_API_KEY not set - using fallback title generation"
|
||||
end
|
||||
|
||||
docs = []
|
||||
|
||||
puts "Reading local docs..."
|
||||
BASE = "#{__dir__}/../../docs_sp/"
|
||||
|
||||
# Generate index.md for each folder that doesn't have one
|
||||
folders_needing_index = Set.new
|
||||
Dir.glob("**/", base: BASE).each do |folder|
|
||||
next if folder == "./" || folder.empty?
|
||||
|
||||
folder_path = folder.chomp("/")
|
||||
index_path = File.join(BASE, folder_path, "index.md")
|
||||
|
||||
unless File.exist?(index_path)
|
||||
folders_needing_index.add(folder_path)
|
||||
|
||||
# Get the folder name (last component of the path)
|
||||
folder_name = File.basename(folder_path)
|
||||
|
||||
# Generate the index.md content
|
||||
index_content = generate_folder_index(folder_name)
|
||||
|
||||
# Write the index.md file
|
||||
File.write(index_path, index_content)
|
||||
puts "Generated index.md for folder: #{folder_path}" if VERBOSE
|
||||
end
|
||||
end
|
||||
|
||||
puts "Generated #{folders_needing_index.size} index.md files" if folders_needing_index.any?
|
||||
|
||||
Dir
|
||||
.glob("**/*.md", base: BASE)
|
||||
.each do |path|
|
||||
next if path.end_with?("index.md")
|
||||
next if path.include?("SAFETY")
|
||||
|
||||
content = File.read(File.join(BASE, path))
|
||||
|
||||
frontmatter, content = Util.parse_md(content)
|
||||
|
||||
# Convert MkDocs Material callouts to Obsidian format
|
||||
content = convert_mkdocs_tabs_to_callouts(content)
|
||||
content = convert_mkdocs_to_obsidian_callouts(content)
|
||||
content = convert_material_icons_to_emojis(content)
|
||||
|
||||
# Generate missing frontmatter fields dynamically
|
||||
generated = generate_frontmatter_from_path(path, content, gemini_client)
|
||||
|
||||
# Apply the generated values, ensuring ID is limited to 50 chars
|
||||
frontmatter["id"] = generated["id"]
|
||||
frontmatter["title"] ||= generated["title"]
|
||||
frontmatter["short_title"] ||= generated["short_title"]
|
||||
|
||||
puts "Generated frontmatter for '#{path}': id='#{frontmatter["id"]}', title='#{frontmatter["title"]}'" if VERBOSE
|
||||
|
||||
docs.push(LocalDoc.new(frontmatter:, path:, content:))
|
||||
end
|
||||
|
||||
puts "Rewriting internal links..."
|
||||
docs.each do |doc|
|
||||
doc.content = rewrite_internal_links(doc.content, docs)
|
||||
end
|
||||
|
||||
puts "Validating local docs..."
|
||||
docs
|
||||
.group_by { |doc| doc.external_id }
|
||||
.each do |id, docs|
|
||||
if docs.size > 1
|
||||
puts "- duplicate external_id '#{id}' found in:"
|
||||
docs.each { |doc| puts "- #{doc.path}" }
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
exit 0 if !DOCS_API_KEY
|
||||
|
||||
puts "Fetching remote info via data-explorer..."
|
||||
remote_topics = API.fetch_current_state
|
||||
|
||||
puts "Mapping to existing topics..."
|
||||
map_to_remote =
|
||||
lambda do
|
||||
docs.each do |doc|
|
||||
puts "- checking '#{doc.external_id}'..." if VERBOSE
|
||||
if topic_info = remote_topics.find { |t| t[:external_id] == doc.external_id }
|
||||
doc.topic_id = topic_info[:topic_id]
|
||||
doc.first_post_id = topic_info[:first_post_id]
|
||||
doc.remote_title = topic_info[:title]
|
||||
doc.remote_content = topic_info[:raw]
|
||||
doc.remote_deleted = topic_info[:deleted_at]
|
||||
puts " found topic_id: #{doc.topic_id}" if VERBOSE
|
||||
else
|
||||
puts " not found" if VERBOSE
|
||||
end
|
||||
end
|
||||
end
|
||||
map_to_remote.call
|
||||
|
||||
puts "Deleting topics if necessary..."
|
||||
|
||||
cat_desc_topic = remote_topics.find { |t| t[:is_index_topic] }
|
||||
if cat_desc_topic.nil?
|
||||
puts "Docs category is missing an index topic"
|
||||
exit 1
|
||||
end
|
||||
cat_desc_topic_id = cat_desc_topic[:topic_id]
|
||||
|
||||
remote_topics
|
||||
.reject { |remote_doc| remote_doc[:deleted_at] }
|
||||
.reject { |remote_doc| docs.any? { |doc| doc.topic_id == remote_doc[:topic_id] } }
|
||||
.reject { |remote_doc| remote_doc[:topic_id] == cat_desc_topic_id }
|
||||
.each do |remote_doc|
|
||||
id = remote_doc[:topic_id]
|
||||
puts "- deleting topic #{id}..."
|
||||
API.trash_topic(topic_id: id)
|
||||
end
|
||||
|
||||
puts "Restoring topics if necessary..."
|
||||
docs
|
||||
.filter(&:remote_deleted)
|
||||
.each do |doc|
|
||||
puts "- restoring '#{doc.external_id}'..."
|
||||
API.restore_topic(topic_id: doc.topic_id)
|
||||
end
|
||||
|
||||
puts "Creating missing topics..."
|
||||
created_any = false
|
||||
docs.each do |doc|
|
||||
next if doc.topic_id
|
||||
|
||||
created_any = true
|
||||
puts "- creating '#{doc.external_id} with title '#{doc.frontmatter["title"]}'..."
|
||||
converted_content = convert_mkdocs_to_obsidian_callouts(doc.content_with_uploads)
|
||||
API.create_topic(
|
||||
external_id: doc.external_id,
|
||||
raw: converted_content,
|
||||
category: CATEGORY_ID,
|
||||
title: doc.frontmatter["title"]
|
||||
)
|
||||
rescue Faraday::UnprocessableEntityError => e
|
||||
puts " 422 error: #{e.response[:body]}"
|
||||
raise e
|
||||
end
|
||||
|
||||
if created_any
|
||||
puts "Re-fetching remote info..."
|
||||
remote_topics = API.fetch_current_state
|
||||
map_to_remote.call
|
||||
end
|
||||
|
||||
puts "Updating content..."
|
||||
docs.each do |doc|
|
||||
if doc.topic_id.nil?
|
||||
next if DRY_RUN
|
||||
raise "Topic ID not found for '#{doc.external_id}'. Something went wrong with creating it?"
|
||||
end
|
||||
|
||||
# Convert callouts in the content before comparison and upload
|
||||
converted_content = convert_mkdocs_to_obsidian_callouts(doc.content_with_uploads)
|
||||
|
||||
if converted_content.strip == doc.remote_content.strip &&
|
||||
doc.frontmatter["title"] == doc.remote_title
|
||||
puts "- no changes required for '#{doc.external_id}' (topic_id: #{doc.topic_id})" if VERBOSE
|
||||
next
|
||||
end
|
||||
|
||||
puts "- updating '#{doc.external_id}' (topic_id: #{doc.topic_id})... new title: '#{doc.frontmatter["title"]}'"
|
||||
API.edit_post(
|
||||
post_id: doc.first_post_id,
|
||||
raw: converted_content,
|
||||
title: doc.frontmatter["title"],
|
||||
category: CATEGORY_ID
|
||||
)
|
||||
rescue Faraday::UnprocessableEntityError => e
|
||||
puts " 422 error: #{e.response[:body]}"
|
||||
raise e
|
||||
end
|
||||
|
||||
puts "Building index..."
|
||||
_, index_content = Util.parse_md(File.read("#{BASE}index.md"))
|
||||
index_content += "\n\n"
|
||||
docs
|
||||
.group_by { |doc| doc.section }
|
||||
.each do |section, section_docs|
|
||||
if section
|
||||
section_frontmatter, _ = Util.parse_md(File.read("#{BASE}#{section}/index.md"))
|
||||
index_content += "## #{section_frontmatter["title"]}\n\n"
|
||||
end
|
||||
|
||||
section_docs.each do |doc|
|
||||
index_content +=
|
||||
"- #{doc.frontmatter["short_title"]}: [#{doc.frontmatter["title"]}](/t/-/#{doc.topic_id}?silent=true)\n"
|
||||
end
|
||||
index_content += "\n"
|
||||
end
|
||||
|
||||
index_post_info = remote_topics.find { |t| t[:topic_id] == cat_desc_topic_id }
|
||||
|
||||
if index_post_info[:raw].strip == index_content.strip
|
||||
puts "- no changes required for index"
|
||||
else
|
||||
puts "- updating index..."
|
||||
API.edit_post(post_id: index_post_info[:first_post_id], raw: index_content)
|
||||
end
|
||||
|
||||
if WATCH
|
||||
puts "Watching for changes to files..."
|
||||
|
||||
Listen
|
||||
.to("#{__dir__}/docs") do |modified, added, removed|
|
||||
if added.size > 0 || removed.size > 0
|
||||
puts "Files added/removed. Restarting sync..."
|
||||
exec("ruby", "#{__dir__}/sync_docs", *ARGV)
|
||||
end
|
||||
|
||||
modified.each do |path|
|
||||
relative = path.sub(BASE, "")
|
||||
doc = docs.find { |d| d.path == relative }
|
||||
raise "Modified file not recognized: #{relative}" if doc.nil?
|
||||
|
||||
print "- updating '#{doc.external_id}' (topic_id: #{doc.topic_id})..."
|
||||
new_frontmatter, new_content = Util.parse_md(File.read(path))
|
||||
if %w[id short_title].any? { |key| doc.frontmatter[key] != new_frontmatter[key] }
|
||||
puts "Frontmatter changed. Restarting sync..."
|
||||
exec("ruby", "#{__dir__}/sync_docs", *ARGV)
|
||||
end
|
||||
doc.content, doc.frontmatter = new_content, new_frontmatter
|
||||
|
||||
# Convert callouts before uploading
|
||||
converted_content = convert_mkdocs_to_obsidian_callouts(doc.content_with_uploads)
|
||||
|
||||
API.edit_post(
|
||||
post_id: doc.first_post_id,
|
||||
raw: converted_content,
|
||||
title: doc.frontmatter["title"]
|
||||
)
|
||||
puts " done"
|
||||
end
|
||||
end
|
||||
.start
|
||||
|
||||
sleep
|
||||
else
|
||||
puts "Done."
|
||||
end
|
||||
BIN
selfdrive/assets/sounds/prompt_single_high.wav
LFS
Normal file
BIN
selfdrive/assets/sounds/prompt_single_high.wav
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/sounds/prompt_single_low.wav
LFS
Normal file
BIN
selfdrive/assets/sounds/prompt_single_low.wav
LFS
Normal file
Binary file not shown.
@@ -57,7 +57,7 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
|
||||
struct_dataclass = structs.CarControlSP(**remove_deprecated({k: v for k, v in struct_dict.items() if not isinstance(k, dict)}))
|
||||
|
||||
struct_dataclass.mads = structs.ModularAssistiveDrivingSystem(**remove_deprecated(struct_dict.get('mads', {})))
|
||||
struct_dataclass.params = [structs.CarControlSP.Param(**remove_deprecated(p)) for p in struct_dict.get('params', [])]
|
||||
# struct_dataclass.params = [structs.CarControlSP.Param(**remove_deprecated(p)) for p in struct_dict.get('params', [])]
|
||||
struct_dataclass.leadOne = structs.LeadData(**remove_deprecated(struct_dict.get('leadOne', {})))
|
||||
struct_dataclass.leadTwo = structs.LeadData(**remove_deprecated(struct_dict.get('leadTwo', {})))
|
||||
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -48,6 +48,10 @@ class LatControlTorque(LatControl):
|
||||
self.lateral_accel_from_torque(-self.steer_max, self.torque_params))
|
||||
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
||||
# Override torque params from extension
|
||||
if self.extension.update_override_torque_params(self.torque_params):
|
||||
self.update_limits()
|
||||
|
||||
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
||||
if not active:
|
||||
output_torque = 0.0
|
||||
|
||||
@@ -11,6 +11,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.selfdrive.locationd.torqued_ext import TorqueEstimatorExt
|
||||
|
||||
HISTORY = 5 # secs
|
||||
POINTS_PER_BUCKET = 1500
|
||||
@@ -50,9 +51,10 @@ class TorqueBuckets(PointBuckets):
|
||||
break
|
||||
|
||||
|
||||
class TorqueEstimator(ParameterEstimator):
|
||||
class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
|
||||
def __init__(self, CP, decimated=False, track_all_points=False):
|
||||
super().__init__()
|
||||
ParameterEstimator.__init__(self)
|
||||
TorqueEstimatorExt.__init__(self, CP)
|
||||
self.CP = CP
|
||||
self.hist_len = int(HISTORY / DT_MDL)
|
||||
self.lag = 0.0
|
||||
@@ -82,6 +84,8 @@ class TorqueEstimator(ParameterEstimator):
|
||||
|
||||
self.calibrator = PoseCalibrator()
|
||||
|
||||
TorqueEstimatorExt.initialize_custom_params(self, decimated)
|
||||
|
||||
self.reset()
|
||||
|
||||
initial_params = {
|
||||
@@ -260,6 +264,8 @@ def main(demo=False):
|
||||
t = sm.logMonoTime[which] * 1e-9
|
||||
estimator.handle_log(t, which, sm[which])
|
||||
|
||||
TorqueEstimatorExt.update_use_params(estimator)
|
||||
|
||||
# 4Hz driven by livePose
|
||||
if sm.frame % 5 == 0:
|
||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks()))
|
||||
|
||||
@@ -24,6 +24,7 @@ from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroa
|
||||
from openpilot.system.version import get_build_metadata
|
||||
|
||||
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
|
||||
from openpilot.sunnypilot import get_sanitize_int_param
|
||||
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
||||
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
||||
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
||||
@@ -43,6 +44,7 @@ LaneChangeDirection = log.LaneChangeDirection
|
||||
EventName = log.OnroadEvent.EventName
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
SafetyModel = car.CarParams.SafetyModel
|
||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||
|
||||
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
|
||||
|
||||
@@ -130,7 +132,12 @@ class SelfdriveD(CruiseHelper):
|
||||
self.logged_comm_issue = None
|
||||
self.not_running_prev = None
|
||||
self.experimental_mode = False
|
||||
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
||||
self.personality = get_sanitize_int_param(
|
||||
"LongitudinalPersonality",
|
||||
min(log.LongitudinalPersonality.schema.enumerants.values()),
|
||||
max(log.LongitudinalPersonality.schema.enumerants.values()),
|
||||
self.params
|
||||
)
|
||||
self.recalibrating_seen = False
|
||||
self.state_machine = StateMachine()
|
||||
self.rk = Ratekeeper(100, print_delay_threshold=None)
|
||||
@@ -299,9 +306,9 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
# Handle lane turn
|
||||
lane_turn_direction = self.sm['modelDataV2SP'].laneTurnDirection
|
||||
if lane_turn_direction == custom.TurnDirection.turnLeft:
|
||||
if lane_turn_direction == TurnDirection.turnLeft:
|
||||
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnLeft)
|
||||
elif lane_turn_direction == custom.TurnDirection.turnRight:
|
||||
elif lane_turn_direction == TurnDirection.turnRight:
|
||||
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnRight)
|
||||
|
||||
for i, pandaState in enumerate(self.sm['pandaStates']):
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,8 +51,8 @@ void HudRenderer::draw(QPainter &p, const QRect &surface_rect) {
|
||||
if (is_cruise_available) {
|
||||
drawSetSpeed(p, surface_rect);
|
||||
}
|
||||
#endif
|
||||
drawCurrentSpeed(p, surface_rect);
|
||||
#endif
|
||||
|
||||
p.restore();
|
||||
}
|
||||
|
||||
@@ -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.;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/alerts.h"
|
||||
#define UIState UIStateSP
|
||||
#define AnnotatedCameraWidget AnnotatedCameraWidgetSP
|
||||
#define OnroadAlerts OnroadAlertsSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/onroad/annotated_camera.h"
|
||||
#endif
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -38,6 +38,7 @@ qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/trips_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/vehicle_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/visuals_panel.cc",
|
||||
"sunnypilot/qt/onroad/alerts.cc",
|
||||
"sunnypilot/qt/onroad/annotated_camera.cc",
|
||||
"sunnypilot/qt/onroad/buttons.cc",
|
||||
"sunnypilot/qt/onroad/developer_ui/developer_ui.cc",
|
||||
@@ -51,6 +52,8 @@ lateral_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/mads_settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.cc",
|
||||
]
|
||||
|
||||
longitudinal_panel_qt_src = [
|
||||
|
||||
@@ -25,8 +25,8 @@ const QMap<QString, QString> Brightness::brightness_options = {
|
||||
|
||||
Brightness::Brightness() : OptionControlSP(
|
||||
"Brightness",
|
||||
tr("Brightness"),
|
||||
tr("Overrides the brightness of the device."),
|
||||
tr("Global Brightness"),
|
||||
tr("Overrides the brightness of the device. This applies to both onroad and offroad screens. "),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
{0, 11}, 1, true, &brightness_options) {
|
||||
|
||||
|
||||
@@ -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");
|
||||
bool is_tested = params.getBool("IsTestedBranch");
|
||||
bool is_development = params.getBool("IsDevelopmentBranch");
|
||||
|
||||
prebuiltToggle->setVisible(!is_release && !is_tested && !is_development);
|
||||
prebuiltToggle->setEnabled(disable_updates);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -23,6 +23,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
||||
{"regulatoryBtn", tr("Regulatory"), ""},
|
||||
{"translateBtn", tr("Language"), ""},
|
||||
{"resetParams", tr("Reset Settings"), ""},
|
||||
{"onroadUploadsBtn", tr("Onroad Uploads"), "OnroadUploads"}
|
||||
};
|
||||
|
||||
int row = 0, col = 0;
|
||||
@@ -75,35 +76,21 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
||||
|
||||
connect(buttons["resetParams"], &PushButtonSP::clicked, this, &DevicePanelSP::resetSettings);
|
||||
|
||||
connect(buttons["onroadUploadsBtn"], &PushButtonSP::clicked, buttons["onroadUploadsBtn"], &PushButtonSP::updateButton);
|
||||
|
||||
// Max Time Offroad
|
||||
maxTimeOffroad = new MaxTimeOffroad();
|
||||
connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh);
|
||||
addItem(maxTimeOffroad);
|
||||
|
||||
toggleDeviceBootMode = new ButtonParamControlSP("DeviceBootMode", tr("Wake-Up Behavior"), "", "", {"Default", "Offroad"}, 375, true);
|
||||
toggleDeviceBootMode = new ButtonParamControlSP("DeviceBootMode", tr("Wake-Up Behavior"), "", "", {"Default", "Offroad"}, 375, true);
|
||||
addItem(toggleDeviceBootMode);
|
||||
|
||||
connect(toggleDeviceBootMode, &ButtonParamControlSP::buttonClicked, this, [=](int index) {
|
||||
params.put("DeviceBootMode", QString::number(index).toStdString());
|
||||
updateState();
|
||||
updateState(offroad);
|
||||
});
|
||||
|
||||
interactivityTimeout = new OptionControlSP("InteractivityTimeout", tr("Interactivity Timeout"),
|
||||
tr("Apply a custom timeout for settings UI."
|
||||
"\nThis is the time after which settings UI closes automatically if user is not interacting with the screen."),
|
||||
"", {0, 120}, 10, true, nullptr, false);
|
||||
|
||||
connect(interactivityTimeout, &OptionControlSP::updateLabels, [=]() {
|
||||
updateState();
|
||||
});
|
||||
|
||||
addItem(interactivityTimeout);
|
||||
|
||||
// Brightness
|
||||
brightness = new Brightness();
|
||||
connect(brightness, &OptionControlSP::updateLabels, brightness, &Brightness::refresh);
|
||||
addItem(brightness);
|
||||
|
||||
addItem(device_grid_layout);
|
||||
|
||||
// offroad mode and power buttons
|
||||
@@ -129,29 +116,21 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
||||
offroadBtn->setFixedWidth(power_layout->sizeHint().width());
|
||||
QObject::connect(offroadBtn, &PushButtonSP::clicked, this, &DevicePanelSP::setOffroadMode);
|
||||
|
||||
QVBoxLayout *power_group_layout = new QVBoxLayout();
|
||||
power_group_layout = new QVBoxLayout();
|
||||
power_group_layout->setSpacing(25);
|
||||
power_group_layout->addWidget(offroadBtn, 0, Qt::AlignHCenter);
|
||||
power_group_layout->addLayout(power_layout);
|
||||
|
||||
addItem(power_group_layout);
|
||||
|
||||
std::vector always_enabled_btns = {
|
||||
always_enabled_btns = {
|
||||
rebootBtn,
|
||||
poweroffBtn,
|
||||
offroadBtn,
|
||||
buttons["quietModeBtn"],
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &DevicePanelSP::updateState);
|
||||
}
|
||||
|
||||
void DevicePanelSP::setOffroadMode() {
|
||||
@@ -175,7 +154,7 @@ void DevicePanelSP::setOffroadMode() {
|
||||
ConfirmationDialog::alert(tr("Disengage to Enter Always Offroad Mode"), this);
|
||||
}
|
||||
|
||||
updateState();
|
||||
updateState(offroad);
|
||||
}
|
||||
|
||||
void DevicePanelSP::resetSettings() {
|
||||
@@ -192,16 +171,20 @@ 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");
|
||||
offroadBtn->setText(offroad_mode_param ? tr("Exit Always Offroad") : tr("Always Offroad"));
|
||||
offroadBtn->setText(offroad_mode_param ? tr("Exit Always Offroad") : tr("Enable Always Offroad"));
|
||||
offroadBtn->setStyleSheet(offroad_mode_param ? alwaysOffroadStyle : autoOffroadStyle);
|
||||
|
||||
DeviceSleepModeStatus currStatus = DeviceSleepModeStatus::DEFAULT;
|
||||
@@ -210,10 +193,11 @@ void DevicePanelSP::updateState() {
|
||||
}
|
||||
toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus));
|
||||
|
||||
QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout"));
|
||||
if (timeoutValue == "0" || timeoutValue.isEmpty()) {
|
||||
interactivityTimeout->setLabel("Default");
|
||||
if (offroad and not offroad_mode_param) {
|
||||
power_group_layout->insertWidget(0, offroadBtn, 0, Qt::AlignHCenter);
|
||||
} else {
|
||||
interactivityTimeout->setLabel(timeoutValue + "s");
|
||||
AddWidgetAt(0, offroadBtn);
|
||||
}
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
@@ -24,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:
|
||||
@@ -32,8 +31,10 @@ private:
|
||||
PushButtonSP *offroadBtn;
|
||||
MaxTimeOffroad *maxTimeOffroad;
|
||||
ButtonParamControlSP *toggleDeviceBootMode;
|
||||
Brightness *brightness;
|
||||
OptionControlSP *interactivityTimeout;
|
||||
QVBoxLayout *power_group_layout;
|
||||
bool offroad;
|
||||
|
||||
std::vector<PushButtonSP*> always_enabled_btns = {};
|
||||
|
||||
const QString alwaysOffroadStyle = R"(
|
||||
PushButtonSP {
|
||||
|
||||
@@ -12,30 +12,29 @@ OnroadScreenBrightnessControl::OnroadScreenBrightnessControl(const QString ¶
|
||||
QWidget *parent)
|
||||
: ExpandableToggleRow(param, title, description, icon, parent) {
|
||||
auto *mainFrame = new QFrame(this);
|
||||
auto *mainFrameLayout = new QGridLayout();
|
||||
auto *mainFrameLayout = new QVBoxLayout();
|
||||
mainFrame->setLayout(mainFrameLayout);
|
||||
mainFrameLayout->setSpacing(0);
|
||||
mainFrameLayout->setSpacing(30);
|
||||
mainFrameLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
onroadScreenOffTimer = new OptionControlSP(
|
||||
"OnroadScreenOffTimer",
|
||||
"",
|
||||
"Onroad Brightness Delay",
|
||||
"",
|
||||
"",
|
||||
{0, 11}, 1, true, &onroadScreenOffTimerOptions);
|
||||
|
||||
onroadScreenBrightness = new OptionControlSP(
|
||||
"OnroadScreenOffBrightness",
|
||||
"Onroad Brightness",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
{0, 100}, 10, true, nullptr, false);
|
||||
{0, 90}, 10, true);
|
||||
|
||||
connect(onroadScreenOffTimer, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);
|
||||
connect(onroadScreenBrightness, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);
|
||||
onroadScreenOffTimer->setFixedWidth(280);
|
||||
onroadScreenBrightness->setFixedWidth(280);
|
||||
mainFrameLayout->addWidget(onroadScreenOffTimer, 0, 0, Qt::AlignLeft);
|
||||
mainFrameLayout->addWidget(onroadScreenBrightness, 0, 1, Qt::AlignRight);
|
||||
mainFrameLayout->addWidget(onroadScreenBrightness);
|
||||
mainFrameLayout->addWidget(onroadScreenOffTimer);
|
||||
|
||||
addItem(mainFrame);
|
||||
|
||||
@@ -45,19 +44,11 @@ OnroadScreenBrightnessControl::OnroadScreenBrightnessControl(const QString ¶
|
||||
void OnroadScreenBrightnessControl::refresh() {
|
||||
// Driving Screen Off Timer
|
||||
int valTimer = std::atoi(params.get("OnroadScreenOffTimer").c_str());
|
||||
std::string labelTimer = "<span style='font-size: 45px; font-weight: 450; color: #FFFFFF;'>";
|
||||
labelTimer += "Delay";
|
||||
labelTimer += " <br><span style='font-size: 40px; font-weight: 450; color:rgb(174, 255, 195);'>";
|
||||
labelTimer += (valTimer < 60 ? std::to_string(valTimer) + "s" : std::to_string(valTimer / 60) + "m");
|
||||
labelTimer += "</span></span>";
|
||||
std::string labelTimer = (valTimer < 60 ? std::to_string(valTimer) + "s" : std::to_string(valTimer / 60) + "m");
|
||||
onroadScreenOffTimer->setLabel(QString::fromStdString(labelTimer));
|
||||
|
||||
// Driving Screen Off Brightness
|
||||
std::string valBrightness = params.get("OnroadScreenOffBrightness");
|
||||
std::string labelBrightness = "<span style='font-size: 45px; font-weight: 450; color: #FFFFFF;'>";
|
||||
labelBrightness += "Brightness";
|
||||
labelBrightness += " <br><span style='font-size: 40px; font-weight: 450; color:rgb(174, 255, 195);'>";
|
||||
labelBrightness += (valBrightness == "0" ? " Screen Off" : valBrightness + "%");
|
||||
labelBrightness += "</span></span>";
|
||||
std::string labelBrightness = (valBrightness == "0" ? " Screen Off" : valBrightness + "%");
|
||||
onroadScreenBrightness->setLabel(QString::fromStdString(labelBrightness));
|
||||
}
|
||||
|
||||
@@ -18,12 +18,30 @@ DisplayPanel::DisplayPanel(QWidget *parent) : QWidget(parent) {
|
||||
// Onroad Screen Off/Brightness
|
||||
onroadScreenBrightnessControl = new OnroadScreenBrightnessControl(
|
||||
"OnroadScreenOffControl",
|
||||
tr("Driving Screen Off: Non-Critical Events"),
|
||||
tr("Onroad Screen: Reduced Brightness"),
|
||||
tr("Turn off device screen or reduce brightness after driving starts. "
|
||||
"It automatically brightens again when screen is touched or a critical event occurs."),
|
||||
"It automatically brightens again when screen is touched or a visible alert is displayed."),
|
||||
"",
|
||||
this);
|
||||
list->addItem(onroadScreenBrightnessControl);
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// Global Brightness
|
||||
brightness = new Brightness();
|
||||
connect(brightness, &OptionControlSP::updateLabels, brightness, &Brightness::refresh);
|
||||
list->addItem(brightness);
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// Interactivity Timeout
|
||||
interactivityTimeout = new OptionControlSP("InteractivityTimeout", tr("Interactivity Timeout"),
|
||||
tr("Apply a custom timeout for settings UI."
|
||||
"\nThis is the time after which settings UI closes automatically if user is not interacting with the screen."),
|
||||
"", {0, 120}, 10, true, nullptr, false);
|
||||
|
||||
connect(interactivityTimeout, &OptionControlSP::updateLabels, [=]() {
|
||||
refresh();
|
||||
});
|
||||
list->addItem(interactivityTimeout);
|
||||
|
||||
sunnypilotScroller = new ScrollViewSP(list, this);
|
||||
vlayout->addWidget(sunnypilotScroller);
|
||||
@@ -37,4 +55,11 @@ void DisplayPanel::showEvent(QShowEvent *event) {
|
||||
|
||||
void DisplayPanel::refresh() {
|
||||
onroadScreenBrightnessControl->refresh();
|
||||
|
||||
QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout"));
|
||||
if (timeoutValue == "0" || timeoutValue.isEmpty()) {
|
||||
interactivityTimeout->setLabel("Default");
|
||||
} else {
|
||||
interactivityTimeout->setLabel(timeoutValue + "s");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/display/onroad_screen_brightness.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
@@ -25,4 +26,6 @@ private:
|
||||
ScrollViewSP *sunnypilotScroller = nullptr;
|
||||
Params params;
|
||||
OnroadScreenBrightnessControl *onroadScreenBrightnessControl = nullptr;
|
||||
Brightness *brightness;
|
||||
OptionControlSP *interactivityTimeout;
|
||||
};
|
||||
|
||||
@@ -10,14 +10,15 @@
|
||||
NeuralNetworkLateralControl::NeuralNetworkLateralControl() :
|
||||
ParamControl("NeuralNetworkLateralControl", tr("Neural Network Lateral Control (NNLC)"), "", "") {
|
||||
setConfirmation(true, false);
|
||||
updateToggle();
|
||||
updateToggle(offroad);
|
||||
}
|
||||
|
||||
void NeuralNetworkLateralControl::updateToggle() {
|
||||
void NeuralNetworkLateralControl::updateToggle(bool _offroad) {
|
||||
QString statusInitText = "<font color='yellow'>" + STATUS_CHECK_COMPATIBILITY + "</font>";
|
||||
QString notLoadedText = "<font color='yellow'>" + STATUS_NOT_LOADED + "</font>";
|
||||
QString loadedText = "<font color=#00ff00>" + STATUS_LOADED + "</font>";
|
||||
|
||||
bool allowed = true;
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
auto cp_sp_bytes = params.get("CarParamsSPPersistent");
|
||||
if (!cp_bytes.empty() && !cp_sp_bytes.empty()) {
|
||||
@@ -31,7 +32,7 @@ void NeuralNetworkLateralControl::updateToggle() {
|
||||
if (CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE) {
|
||||
params.remove("NeuralNetworkLateralControl");
|
||||
setDescription(nnffDescriptionBuilder(STATUS_NOT_AVAILABLE));
|
||||
setEnabled(false);
|
||||
allowed = false;
|
||||
} else {
|
||||
QString nn_model_name = QString::fromStdString(CP_SP.getNeuralNetworkLateralControl().getModel().getName());
|
||||
QString nn_fuzzy = CP_SP.getNeuralNetworkLateralControl().getFuzzyFingerprint() ?
|
||||
@@ -56,4 +57,11 @@ void NeuralNetworkLateralControl::updateToggle() {
|
||||
if (getDescription() != getBaseDescription()) {
|
||||
showDescription();
|
||||
}
|
||||
|
||||
bool enforce_torque_toggle = params.getBool("EnforceTorqueControl");
|
||||
setEnabled(_offroad && allowed && !enforce_torque_toggle);
|
||||
|
||||
refresh();
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,11 @@ public:
|
||||
NeuralNetworkLateralControl();
|
||||
|
||||
public slots:
|
||||
void updateToggle();
|
||||
void updateToggle(bool _offroad);
|
||||
|
||||
private:
|
||||
Params params;
|
||||
bool offroad;
|
||||
|
||||
// Status messages
|
||||
const QString STATUS_NOT_AVAILABLE = tr("NNLC is currently not available on this platform.");
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.h"
|
||||
|
||||
TorqueLateralControlCustomParams::TorqueLateralControlCustomParams(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent)
|
||||
: ExpandableToggleRow(param, title, description, icon, parent) {
|
||||
|
||||
QFrame *frame = new QFrame(this);
|
||||
QGridLayout *frame_layout = new QGridLayout();
|
||||
frame->setLayout(frame_layout);
|
||||
frame_layout->setSpacing(0);
|
||||
|
||||
torqueLateralControlParamsOverride = new ParamControl(
|
||||
"TorqueParamsOverrideEnabled",
|
||||
tr("Manual Real-Time Tuning"),
|
||||
tr("Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
this
|
||||
);
|
||||
connect(torqueLateralControlParamsOverride, &ParamControl::toggleFlipped, this, &TorqueLateralControlCustomParams::refresh);
|
||||
|
||||
torqueParamsOverrideLatAccelFactor = new OptionControlSP("TorqueParamsOverrideLatAccelFactor", tr("Lateral Acceleration Factor"), "", "", {1, 500}, 1, false, nullptr, true, false);
|
||||
connect(torqueParamsOverrideLatAccelFactor, &OptionControlSP::updateLabels, this, &TorqueLateralControlCustomParams::refresh);
|
||||
torqueParamsOverrideLatAccelFactor->setFixedWidth(280);
|
||||
|
||||
torqueParamsOverrideFriction = new OptionControlSP("TorqueParamsOverrideFriction", tr("Friction"), "", "", {1, 100}, 1, false, nullptr, true, false);
|
||||
connect(torqueParamsOverrideFriction, &OptionControlSP::updateLabels, this, &TorqueLateralControlCustomParams::refresh);
|
||||
torqueParamsOverrideFriction->setFixedWidth(280);
|
||||
|
||||
frame_layout->addWidget(torqueLateralControlParamsOverride, 0, 0, 1, 2);
|
||||
QSpacerItem *spacer = new QSpacerItem(20, 40);
|
||||
frame_layout->addItem(spacer, 1, 0, 1, 2);
|
||||
frame_layout->addWidget(torqueParamsOverrideLatAccelFactor, 2, 0, Qt::AlignCenter);
|
||||
frame_layout->addWidget(torqueParamsOverrideFriction, 2, 1, Qt::AlignCenter);
|
||||
|
||||
addItem(frame);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void TorqueLateralControlCustomParams::refresh() {
|
||||
bool torque_override_param = params.getBool("TorqueParamsOverrideEnabled");
|
||||
float laf_param = QString::fromStdString(params.get("TorqueParamsOverrideLatAccelFactor")).toFloat();
|
||||
const QString laf_unit = "m/s²";
|
||||
|
||||
float friction_param = QString::fromStdString(params.get("TorqueParamsOverrideFriction")).toFloat();
|
||||
|
||||
torqueParamsOverrideLatAccelFactor->setTitle(tr("Lateral Acceleration Factor") + "\n(" + (torque_override_param ? tr("Real-time and Offline") : tr("Offline Only")) + ")");
|
||||
torqueParamsOverrideFriction->setTitle(tr("Friction") + "\n(" + (torque_override_param ? tr("Real-time and Offline") : tr("Offline Only")) + ")");
|
||||
|
||||
torqueParamsOverrideLatAccelFactor->setLabel(QString::number(laf_param, 'f', 2) + " " + laf_unit);
|
||||
torqueParamsOverrideFriction->setLabel(QString::number(friction_param, 'f', 2));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h"
|
||||
|
||||
class TorqueLateralControlCustomParams : public ExpandableToggleRow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TorqueLateralControlCustomParams(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent = nullptr);
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
ParamControl *torqueLateralControlParamsOverride;
|
||||
OptionControlSP *torqueParamsOverrideFriction;
|
||||
OptionControlSP *torqueParamsOverrideLatAccelFactor;
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
|
||||
TorqueLateralControlSettings::TorqueLateralControlSettings(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(50, 20, 50, 20);
|
||||
main_layout->setSpacing(20);
|
||||
|
||||
// Back button
|
||||
PanelBackButton *back = new PanelBackButton();
|
||||
connect(back, &QPushButton::clicked, [=]() { emit backPress(); });
|
||||
main_layout->addWidget(back, 0, Qt::AlignLeft);
|
||||
|
||||
ListWidget *list = new ListWidget(this, false);
|
||||
// param, title, desc, icon
|
||||
std::vector<std::tuple<QString, QString, QString, QString>> toggle_defs{
|
||||
{
|
||||
"LiveTorqueParamsToggle",
|
||||
tr("Self-Tune"),
|
||||
tr("Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
},
|
||||
{
|
||||
"LiveTorqueParamsRelaxedToggle",
|
||||
tr("Less Restrict Settings for Self-Tune (Beta)"),
|
||||
tr("Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
}
|
||||
};
|
||||
|
||||
for (auto &[param, title, desc, icon] : toggle_defs) {
|
||||
auto toggle = new ParamControlSP(param, title, desc, icon, this);
|
||||
list->addItem(toggle);
|
||||
toggles[param.toStdString()] = toggle;
|
||||
}
|
||||
|
||||
torqueLateralControlCustomParams = new TorqueLateralControlCustomParams(
|
||||
"CustomTorqueParams",
|
||||
tr("Enable Custom Tuning"),
|
||||
tr("Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below will override the offline values indicated in the YAML files within \"opendbc/car/torque_data\". "
|
||||
"The values will also be used live when \"Manual Real-Time Tuning\" toggle is enabled."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
this);
|
||||
list->addItem(torqueLateralControlCustomParams);
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &TorqueLateralControlSettings::updateToggles);
|
||||
QObject::connect(toggles["LiveTorqueParamsToggle"], &ParamControlSP::toggleFlipped, [=](bool state) {
|
||||
if (!state) {
|
||||
params.remove("LiveTorqueParamsRelaxedToggle");
|
||||
toggles["LiveTorqueParamsRelaxedToggle"]->refresh();
|
||||
}
|
||||
|
||||
updateToggles(offroad);
|
||||
});
|
||||
|
||||
main_layout->addWidget(new ScrollViewSP(list, this));
|
||||
}
|
||||
|
||||
void TorqueLateralControlSettings::showEvent(QShowEvent *event) {
|
||||
updateToggles(offroad);
|
||||
}
|
||||
|
||||
void TorqueLateralControlSettings::updateToggles(bool _offroad) {
|
||||
bool live_toggle = toggles["LiveTorqueParamsToggle"]->isToggled();
|
||||
|
||||
toggles["LiveTorqueParamsToggle"]->setEnabled(_offroad);
|
||||
toggles["LiveTorqueParamsRelaxedToggle"]->setEnabled(_offroad && live_toggle);
|
||||
|
||||
torqueLateralControlCustomParams->setEnabled(_offroad);
|
||||
torqueLateralControlCustomParams->refresh();
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_custom_params.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
class TorqueLateralControlSettings : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TorqueLateralControlSettings(QWidget *parent = nullptr);
|
||||
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
signals:
|
||||
void backPress();
|
||||
|
||||
public slots:
|
||||
void updateToggles(bool _offroad);
|
||||
|
||||
private:
|
||||
Params params;
|
||||
bool offroad;
|
||||
std::map<std::string, ParamControlSP*> toggles;
|
||||
|
||||
TorqueLateralControlCustomParams *torqueLateralControlCustomParams;
|
||||
};
|
||||
@@ -75,6 +75,36 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// Customized Torque Lateral Control
|
||||
torqueLateralControlToggle = new ParamControl(
|
||||
"EnforceTorqueControl",
|
||||
tr("Enforce Torque Lateral Control"),
|
||||
tr("Enable this to enforce sunnypilot to steer with Torque lateral control."),
|
||||
"");
|
||||
list->addItem(torqueLateralControlToggle);
|
||||
|
||||
torqueLateralControlSettingsButton = new PushButtonSP(tr("Customize Params"));
|
||||
torqueLateralControlSettingsButton->setObjectName("torque_btn");
|
||||
connect(torqueLateralControlSettingsButton, &QPushButton::clicked, [=]() {
|
||||
sunnypilotScroller->setLastScrollPosition();
|
||||
main_layout->setCurrentWidget(torqueLateralControlWidget);
|
||||
});
|
||||
QObject::connect(torqueLateralControlToggle, &ToggleControl::toggleFlipped, [=](bool state) {
|
||||
torqueLateralControlSettingsButton->setEnabled(state);
|
||||
nnlcToggle->updateToggle(offroad);
|
||||
updateToggles(offroad);
|
||||
});
|
||||
|
||||
torqueLateralControlWidget = new TorqueLateralControlSettings(this);
|
||||
connect(torqueLateralControlWidget, &TorqueLateralControlSettings::backPress, [=]() {
|
||||
sunnypilotScroller->restoreScrollPosition();
|
||||
main_layout->setCurrentWidget(sunnypilotScreen);
|
||||
});
|
||||
list->addItem(torqueLateralControlSettingsButton);
|
||||
|
||||
list->addItem(vertical_space(0));
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// Neural Network Lateral Control
|
||||
nnlcToggle = new NeuralNetworkLateralControl();
|
||||
list->addItem(nnlcToggle);
|
||||
@@ -86,12 +116,10 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
nnlcToggle->hideDescription();
|
||||
}
|
||||
|
||||
nnlcToggle->updateToggle();
|
||||
nnlcToggle->updateToggle(offroad);
|
||||
updateToggles(offroad);
|
||||
});
|
||||
|
||||
toggleOffroadOnly = {
|
||||
madsToggle, nnlcToggle,
|
||||
};
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &LateralPanel::updateToggles);
|
||||
|
||||
sunnypilotScroller = new ScrollViewSP(list, this);
|
||||
@@ -100,6 +128,7 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
main_layout->addWidget(sunnypilotScreen);
|
||||
main_layout->addWidget(madsWidget);
|
||||
main_layout->addWidget(laneChangeWidget);
|
||||
main_layout->addWidget(torqueLateralControlWidget);
|
||||
|
||||
setStyleSheet(R"(
|
||||
#back_btn {
|
||||
@@ -120,7 +149,7 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
}
|
||||
|
||||
void LateralPanel::showEvent(QShowEvent *event) {
|
||||
nnlcToggle->updateToggle();
|
||||
nnlcToggle->updateToggle(offroad);
|
||||
updateToggles(offroad);
|
||||
}
|
||||
|
||||
@@ -129,10 +158,7 @@ void LateralPanel::hideEvent(QHideEvent *event) {
|
||||
}
|
||||
|
||||
void LateralPanel::updateToggles(bool _offroad) {
|
||||
for (auto *toggle : toggleOffroadOnly) {
|
||||
toggle->setEnabled(_offroad);
|
||||
}
|
||||
|
||||
bool torque_allowed = true;
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
auto cp_sp_bytes = params.get("CarParamsSPPersistent");
|
||||
if (!cp_bytes.empty() && !cp_sp_bytes.empty()) {
|
||||
@@ -148,12 +174,24 @@ void LateralPanel::updateToggles(bool _offroad) {
|
||||
} else {
|
||||
madsToggle->setDescription(descriptionBuilder(STATUS_MADS_SETTINGS_FULL_COMPATIBILITY, MADS_BASE_DESC));
|
||||
}
|
||||
|
||||
if (CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE) {
|
||||
params.remove("EnforceTorqueControl");
|
||||
torque_allowed = false;
|
||||
}
|
||||
} else {
|
||||
madsToggle->setDescription(descriptionBuilder(STATUS_MADS_CHECK_COMPATIBILITY, MADS_BASE_DESC));
|
||||
|
||||
params.remove("EnforceTorqueControl");
|
||||
torque_allowed = false;
|
||||
}
|
||||
|
||||
madsToggle->setEnabled(_offroad);
|
||||
madsSettingsButton->setEnabled(madsToggle->isToggled());
|
||||
|
||||
torqueLateralControlToggle->setEnabled(_offroad && torque_allowed && !nnlcToggle->isToggled());
|
||||
torqueLateralControlSettingsButton->setEnabled(torqueLateralControlToggle->isToggled());
|
||||
|
||||
blinkerPauseLateralSettings->refresh();
|
||||
|
||||
offroad = _offroad;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/torque_lateral_control_settings.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
@@ -35,7 +36,6 @@ private:
|
||||
QStackedLayout* main_layout = nullptr;
|
||||
QWidget* sunnypilotScreen = nullptr;
|
||||
ScrollViewSP *sunnypilotScroller = nullptr;
|
||||
std::vector<ParamControl *> toggleOffroadOnly;
|
||||
bool offroad;
|
||||
|
||||
ParamControl *madsToggle;
|
||||
@@ -45,6 +45,9 @@ private:
|
||||
LaneChangeSettings *laneChangeWidget = nullptr;
|
||||
NeuralNetworkLateralControl *nnlcToggle = nullptr;
|
||||
BlinkerPauseLateralSettings *blinkerPauseLateralSettings = nullptr;
|
||||
ParamControl *torqueLateralControlToggle;
|
||||
PushButtonSP *torqueLateralControlSettingsButton;
|
||||
TorqueLateralControlSettings *torqueLateralControlWidget = nullptr;
|
||||
|
||||
const QString MADS_BASE_DESC = tr("Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC).");
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ enum class SpeedLimitOffsetType {
|
||||
};
|
||||
|
||||
inline const QString SpeedLimitOffsetTypeTexts[]{
|
||||
QObject::tr("None"),
|
||||
QObject::tr("Fixed"),
|
||||
QObject::tr("Percent"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "None"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Fixed"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Percent"),
|
||||
};
|
||||
|
||||
enum class SpeedLimitSourcePolicy {
|
||||
@@ -27,11 +27,11 @@ enum class SpeedLimitSourcePolicy {
|
||||
};
|
||||
|
||||
inline const QString SpeedLimitSourcePolicyTexts[]{
|
||||
QObject::tr("Car\nOnly"),
|
||||
QObject::tr("Map\nOnly"),
|
||||
QObject::tr("Car\nFirst"),
|
||||
QObject::tr("Map\nFirst"),
|
||||
QObject::tr("Combined\nData")
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nOnly"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nOnly"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nFirst"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nFirst"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Combined\nData")
|
||||
};
|
||||
|
||||
enum class SpeedLimitMode {
|
||||
@@ -42,8 +42,8 @@ enum class SpeedLimitMode {
|
||||
};
|
||||
|
||||
inline const QString SpeedLimitModeTexts[]{
|
||||
QObject::tr("Off"),
|
||||
QObject::tr("Information"),
|
||||
QObject::tr("Warning"),
|
||||
QObject::tr("Assist"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Off"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Information"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Warning"),
|
||||
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Assist"),
|
||||
};
|
||||
|
||||
@@ -23,11 +23,11 @@ SpeedLimitPolicy::SpeedLimitPolicy(QWidget *parent) : QWidget(parent) {
|
||||
ListWidgetSP *list = new ListWidgetSP(this);
|
||||
|
||||
std::vector<QString> speed_limit_policy_texts{
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_ONLY)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_ONLY)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_FIRST)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_FIRST)],
|
||||
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::COMBINED)]
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_ONLY)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_ONLY)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_FIRST)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_FIRST)].toStdString().c_str()),
|
||||
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::COMBINED)].toStdString().c_str())
|
||||
};
|
||||
speed_limit_policy = new ButtonParamControlSP(
|
||||
"SpeedLimitPolicy",
|
||||
|
||||
@@ -25,10 +25,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 +64,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",
|
||||
@@ -122,6 +122,12 @@ void SpeedLimitSettings::refresh() {
|
||||
|
||||
has_longitudinal_control = hasLongitudinalControl(CP);
|
||||
intelligent_cruise_button_management_available = CP_SP.getIntelligentCruiseButtonManagementAvailable();
|
||||
|
||||
if (!has_longitudinal_control && CP_SP.getPcmCruiseSpeed()) {
|
||||
if (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;
|
||||
@@ -148,7 +154,7 @@ void SpeedLimitSettings::refresh() {
|
||||
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->showDescription();
|
||||
@@ -158,3 +164,7 @@ void SpeedLimitSettings::refresh() {
|
||||
void SpeedLimitSettings::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SpeedLimitSettings::hideEvent(QHideEvent *event) {
|
||||
setCurrentWidget(subPanelFrame);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
SpeedLimitSettings(QWidget *parent = nullptr);
|
||||
void refresh();
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
signals:
|
||||
void backPress();
|
||||
|
||||
@@ -43,6 +43,15 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
|
||||
intelligentCruiseButtonManagement->setConfirmation(true, false);
|
||||
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,6 +95,10 @@ void LongitudinalPanel::showEvent(QShowEvent *event) {
|
||||
refresh(offroad);
|
||||
}
|
||||
|
||||
void LongitudinalPanel::hideEvent(QHideEvent *event) {
|
||||
main_layout->setCurrentWidget(cruisePanelScreen);
|
||||
}
|
||||
|
||||
void LongitudinalPanel::refresh(bool _offroad) {
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
auto cp_sp_bytes = params.get("CarParamsSPPersistent");
|
||||
@@ -100,6 +113,17 @@ void LongitudinalPanel::refresh(bool _offroad) {
|
||||
has_longitudinal_control = hasLongitudinalControl(CP);
|
||||
is_pcm_cruise = CP.getPcmCruise();
|
||||
intelligent_cruise_button_management_available = CP_SP.getIntelligentCruiseButtonManagementAvailable();
|
||||
|
||||
if (!intelligent_cruise_button_management_available || has_longitudinal_control) {
|
||||
params.remove("IntelligentCruiseButtonManagement");
|
||||
}
|
||||
|
||||
if (!has_longitudinal_control && CP_SP.getPcmCruiseSpeed()) {
|
||||
params.remove("CustomAccIncrementsEnabled");
|
||||
params.remove("DynamicExperimentalControl");
|
||||
params.remove("SmartCruiseControlVision");
|
||||
params.remove("SmartCruiseControlMap");
|
||||
}
|
||||
} else {
|
||||
has_longitudinal_control = false;
|
||||
is_pcm_cruise = false;
|
||||
@@ -123,11 +147,9 @@ void LongitudinalPanel::refresh(bool _offroad) {
|
||||
customAccIncrement->setDescription(accEnabledDescription);
|
||||
}
|
||||
} else {
|
||||
params.remove("CustomAccIncrementsEnabled");
|
||||
customAccIncrement->toggleFlipped(false);
|
||||
customAccIncrement->setDescription(accNoLongDescription);
|
||||
customAccIncrement->showDescription();
|
||||
params.remove("IntelligentCruiseButtonManagement");
|
||||
intelligentCruiseButtonManagement->toggleFlipped(false);
|
||||
}
|
||||
}
|
||||
@@ -140,6 +162,7 @@ void LongitudinalPanel::refresh(bool _offroad) {
|
||||
customAccIncrement->setEnabled(cai_allowed && !offroad);
|
||||
customAccIncrement->refresh();
|
||||
|
||||
dynamicExperimentalControl->setEnabled(has_longitudinal_control);
|
||||
SmartCruiseControlVision->setEnabled(has_longitudinal_control || icbm_allowed);
|
||||
SmartCruiseControlMap->setEnabled(has_longitudinal_control || icbm_allowed);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ 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:
|
||||
@@ -34,6 +35,7 @@ private:
|
||||
ParamControl *SmartCruiseControlVision;
|
||||
ParamControl *SmartCruiseControlMap;
|
||||
ParamControl *intelligentCruiseButtonManagement = nullptr;
|
||||
ParamControl *dynamicExperimentalControl = nullptr;
|
||||
SpeedLimitSettings *speedLimitScreen;
|
||||
PushButtonSP *speedLimitSettings;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
|
||||
OsmPanel::OsmPanel(QWidget *parent) : QFrame(parent) {
|
||||
@@ -91,24 +92,30 @@ ButtonControlSP *OsmPanel::setupOsmDownloadButton(QWidget *parent) {
|
||||
locationTitles.push_back(std::get<0>(loc));
|
||||
}
|
||||
|
||||
const QString selection = MultiOptionDialog::getSelection(tr("Country"), locationTitles, currentTitle, this);
|
||||
if (!selection.isEmpty()) {
|
||||
params.put("OsmLocal", "1");
|
||||
params.put("OsmLocationTitle", selection.toStdString());
|
||||
for (auto &loc: locations) {
|
||||
if (std::get<0>(loc) == selection) {
|
||||
params.put("OsmLocationName", std::get<1>(loc).toStdString());
|
||||
break;
|
||||
InputDialog d(tr("Search Country"), this, tr("Enter search keywords, or leave blank to list all countries."), false);
|
||||
d.setMinLength(0);
|
||||
const int ret = d.exec();
|
||||
if (ret) {
|
||||
const QString selection = search(d.text(), locationTitles, tr("Select Country"));
|
||||
if (!selection.isEmpty()) {
|
||||
params.put("OsmLocal", "1");
|
||||
params.put("OsmLocationTitle", selection.toStdString());
|
||||
for (auto &loc: locations) {
|
||||
if (std::get<0>(loc) == selection) {
|
||||
params.put("OsmLocationName", std::get<1>(loc).toStdString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (params.get("OsmLocationName") == "US") {
|
||||
usStatesBtn->click();
|
||||
return;
|
||||
} else if (selection != "== None ==") {
|
||||
if (showConfirmationDialog(parent)) {
|
||||
osm_download_in_progress = true;
|
||||
params.putBool("OsmDbUpdatesCheck", true);
|
||||
updateLabels();
|
||||
if (params.get("OsmLocationName") == "US") {
|
||||
usStatesBtn->click();
|
||||
return;
|
||||
}
|
||||
if (selection != "== None ==") {
|
||||
if (showConfirmationDialog(parent)) {
|
||||
osm_download_in_progress = true;
|
||||
params.putBool("OsmDbUpdatesCheck", true);
|
||||
updateLabels();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,20 +142,25 @@ ButtonControlSP *OsmPanel::setupUsStatesButton(QWidget *parent) {
|
||||
locationTitles.push_back(std::get<0>(loc));
|
||||
}
|
||||
|
||||
const QString selection = MultiOptionDialog::getSelection(tr("State"), locationTitles, currentTitle, this);
|
||||
if (!selection.isEmpty()) {
|
||||
params.put("OsmStateTitle", selection.toStdString());
|
||||
for (auto &loc: locations) {
|
||||
if (std::get<0>(loc) == selection) {
|
||||
params.put("OsmStateName", std::get<1>(loc).toStdString());
|
||||
break;
|
||||
InputDialog d(tr("Search State"), this, tr("Enter search keywords, or leave blank to list all states."), false);
|
||||
d.setMinLength(0);
|
||||
const int ret = d.exec();
|
||||
if (ret) {
|
||||
const QString selection = search(d.text(), locationTitles, tr("Select State"));
|
||||
if (!selection.isEmpty()) {
|
||||
params.put("OsmStateTitle", selection.toStdString());
|
||||
for (auto &loc: locations) {
|
||||
if (std::get<0>(loc) == selection) {
|
||||
params.put("OsmStateName", std::get<1>(loc).toStdString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
usStatesBtn->setValue(selection);
|
||||
if (showConfirmationDialog(parent)) {
|
||||
osm_download_in_progress = true;
|
||||
params.putBool("OsmDbUpdatesCheck", true);
|
||||
updateLabels();
|
||||
}
|
||||
}
|
||||
usStatesBtn->setValue(selection);
|
||||
if (showConfirmationDialog(parent)) {
|
||||
osm_download_in_progress = true;
|
||||
params.putBool("OsmDbUpdatesCheck", true);
|
||||
updateLabels();
|
||||
}
|
||||
}
|
||||
updateLabels();
|
||||
@@ -281,3 +293,15 @@ void OsmPanel::updateMapSize() {
|
||||
mapSizeFuture = QtConcurrent::run(getDirSize, MAP_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
QString OsmPanel::search(const QString &query, const QStringList &list, const QString &prompt_text) {
|
||||
QStringList lst_results = searchFromList(query, list);
|
||||
QString selection;
|
||||
|
||||
if (lst_results.isEmpty()) {
|
||||
ConfirmationDialog::alert(tr("No results found for keywords: %1").arg(query), this);
|
||||
return selection;
|
||||
}
|
||||
selection = MultiOptionDialog::getSelection(prompt_text, lst_results, "", this);
|
||||
return selection;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ private:
|
||||
void updateDownloadProgress();
|
||||
static int extractIntFromJson(const QJsonObject &json, const QString &key);
|
||||
QString processUpdateStatus(bool pending_update_check, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state);
|
||||
QString search(const QString &query, const QStringList &list, const QString &prompt_text);
|
||||
|
||||
ConfirmationDialog *confirmationDialog;
|
||||
LabelControlSP *mapdVersion;
|
||||
|
||||
@@ -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) {
|
||||
@@ -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();
|
||||
|
||||
@@ -8,7 +8,52 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h"
|
||||
|
||||
SubaruSettings::SubaruSettings(QWidget *parent) : BrandSettingsInterface(parent) {
|
||||
stopAndGoToggle = new ParamControl("SubaruStopAndGo", tr("Stop and Go (Beta)"), "", "");
|
||||
stopAndGoToggle->setConfirmation(true, false);
|
||||
list->addItem(stopAndGoToggle);
|
||||
|
||||
stopAndGoManualParkingBrakeToggle = new ParamControl(
|
||||
"SubaruStopAndGoManualParkingBrake",
|
||||
tr("Stop and Go for Manual Parking Brake (Beta)"),
|
||||
"",
|
||||
""
|
||||
);
|
||||
stopAndGoManualParkingBrakeToggle->setConfirmation(true, false);
|
||||
list->addItem(stopAndGoManualParkingBrakeToggle);
|
||||
}
|
||||
|
||||
void SubaruSettings::updateSettings() {
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
|
||||
is_subaru = CP.getBrand() == "subaru";
|
||||
|
||||
if (is_subaru) {
|
||||
if (!(CP.getFlags() & (SUBARU_FLAG_GLOBAL_GEN2 | SUBARU_FLAG_HYBRID))) {
|
||||
has_stop_and_go = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
is_subaru = false;
|
||||
has_stop_and_go = false;
|
||||
}
|
||||
|
||||
bool stop_and_go_disabled = !offroad || !has_stop_and_go;
|
||||
QString stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc);
|
||||
QString stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc);
|
||||
if (stop_and_go_disabled) {
|
||||
stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc, stopAndGoDisabledMsg());
|
||||
stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc, stopAndGoDisabledMsg());
|
||||
}
|
||||
|
||||
stopAndGoToggle->setEnabled(has_stop_and_go);
|
||||
stopAndGoToggle->setDescription(stop_and_go_desc);
|
||||
stopAndGoToggle->showDescription();
|
||||
|
||||
stopAndGoManualParkingBrakeToggle->setEnabled(has_stop_and_go);
|
||||
stopAndGoManualParkingBrakeToggle->setDescription(stop_and_go_manual_parking_brake_desc);
|
||||
stopAndGoManualParkingBrakeToggle->showDescription();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
const int SUBARU_FLAG_GLOBAL_GEN2 = 4;
|
||||
const int SUBARU_FLAG_HYBRID = 32;
|
||||
|
||||
class SubaruSettings : public BrandSettingsInterface {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -23,4 +26,32 @@ public:
|
||||
|
||||
private:
|
||||
bool offroad = false;
|
||||
bool is_subaru;
|
||||
bool has_stop_and_go;
|
||||
|
||||
ParamControl* stopAndGoToggle;
|
||||
ParamControl* stopAndGoManualParkingBrakeToggle;
|
||||
|
||||
QString stopAndGoDesc = tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms.");
|
||||
QString stopAndGoManualParkingBrakeDesc = tr("Experimental feature to enable stop and go for Subaru Global models with manual handbrake. Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!");
|
||||
|
||||
QString stopAndGoDisabledMsg() const {
|
||||
if (is_subaru && !has_stop_and_go) {
|
||||
return tr("This feature is currently not available on this platform.");
|
||||
}
|
||||
|
||||
if (!is_subaru) {
|
||||
return tr("Start the car to check car compatibility.");
|
||||
}
|
||||
|
||||
if (!offroad) {
|
||||
return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.");
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
static QString stopAndGoDescriptionBuilder(const QString &base_description, const QString &custom_description = "") {
|
||||
return "<b>" + custom_description + "</b><br><br>" + base_description;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,6 +69,27 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"TrueVEgoUI",
|
||||
tr("Speedometer: Always Display True Speed"),
|
||||
tr("Always display the true vehicle current speed from wheel speed sensors."),
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"HideVEgoUI",
|
||||
tr("Speedometer: Hide from Onroad Screen"),
|
||||
tr("When enabled, the speedometer on the onroad screen is not displayed."),
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"ShowTurnSignals",
|
||||
tr("Display Turn Signals"),
|
||||
tr("When enabled, visual turn indicators are drawn on the HUD."),
|
||||
"",
|
||||
false,
|
||||
},
|
||||
};
|
||||
|
||||
// Add regular toggles first
|
||||
@@ -119,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 openpilot longitudinal control).");
|
||||
QString chevronNoLongDescription = tr("This feature requires openpilot 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() {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
102
selfdrive/ui/sunnypilot/qt/onroad/alerts.cc
Normal file
102
selfdrive/ui/sunnypilot/qt/onroad/alerts.cc
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/alerts.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <map>
|
||||
#include <QRect>
|
||||
#include <QFont>
|
||||
|
||||
OnroadAlerts::Alert OnroadAlertsSP::getAlert(const SubMaster &sm, uint64_t started_frame) {
|
||||
OnroadAlerts::Alert alert = OnroadAlerts::getAlert(sm, started_frame);
|
||||
alert.text1.replace("openpilot", "sunnypilot");
|
||||
alert.text2.replace("openpilot", "sunnypilot");
|
||||
return alert;
|
||||
}
|
||||
|
||||
void OnroadAlertsSP::paintEvent(QPaintEvent *event) {
|
||||
if (alert.size == cereal::SelfdriveState::AlertSize::NONE) {
|
||||
return;
|
||||
} else if (alert.size == cereal::SelfdriveState::AlertSize::FULL) {
|
||||
OnroadAlerts::paintEvent(event);
|
||||
return;
|
||||
}
|
||||
static std::map<cereal::SelfdriveState::AlertSize, const int> alert_heights = {
|
||||
{cereal::SelfdriveState::AlertSize::SMALL, 271},
|
||||
{cereal::SelfdriveState::AlertSize::MID, 420}
|
||||
};
|
||||
int h = alert_heights[alert.size];
|
||||
|
||||
QPainter p(this);
|
||||
QFont topFont;
|
||||
QFont bottomFont;
|
||||
QRect topTextBoundingRect;
|
||||
QRect bottomTextBoundingRect;
|
||||
QRect rect;
|
||||
|
||||
int margin = 40;
|
||||
int radius = 30;
|
||||
|
||||
const int dev_ui_info = uiStateSP()->scene.dev_ui_info;
|
||||
const int v_adjustment = dev_ui_info > 1 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 40 : 0;
|
||||
const int h_adjustment = dev_ui_info > 0 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 230 : 0;
|
||||
|
||||
if (alert.size == cereal::SelfdriveState::AlertSize::SMALL) {
|
||||
topFont = InterFont(74, QFont::DemiBold);
|
||||
QFontMetrics fmTop(topFont);
|
||||
topTextBoundingRect = fmTop.boundingRect(
|
||||
QRect(0 + margin, height() - h + margin - v_adjustment, width() - margin * 2 - h_adjustment, 0), Qt::TextWordWrap,
|
||||
alert.text1);
|
||||
h = topTextBoundingRect.height();
|
||||
rect = QRect(0 + margin, height() - h - margin * 2 - v_adjustment, width() - margin * 2 - h_adjustment, h + margin);
|
||||
} else if (alert.size == cereal::SelfdriveState::AlertSize::MID) {
|
||||
topFont = InterFont(88, QFont::Bold);
|
||||
bottomFont = InterFont(66);
|
||||
QFontMetrics fmTop(topFont);
|
||||
QFontMetrics fmBotton(bottomFont);
|
||||
topTextBoundingRect = fmTop.boundingRect(
|
||||
QRect(0 + margin, height() - h + margin - v_adjustment, width() - margin * 2 - h_adjustment, 0), Qt::TextWordWrap,
|
||||
alert.text1);
|
||||
bottomTextBoundingRect = fmBotton.boundingRect(
|
||||
QRect(0 + margin, height() - h + margin - v_adjustment + topTextBoundingRect.height(),
|
||||
width() - margin * 2 - h_adjustment, 0), Qt::TextWordWrap, alert.text2);
|
||||
h = topTextBoundingRect.height() + bottomTextBoundingRect.height() + margin * 2;
|
||||
rect = QRect(0 + margin, height() - h - margin * 2 - v_adjustment, width() - margin * 2 - h_adjustment, h + margin);
|
||||
}
|
||||
|
||||
|
||||
// draw background + gradient
|
||||
// draw background + gradient
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
p.setBrush(QBrush(alert_colors[alert.status]));
|
||||
p.drawRoundedRect(rect, radius, radius);
|
||||
|
||||
QLinearGradient g(0, rect.y(), 0, rect.bottom());
|
||||
g.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.05));
|
||||
g.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0.35));
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_DestinationOver);
|
||||
p.setBrush(QBrush(g));
|
||||
p.drawRoundedRect(rect, radius, radius);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
|
||||
// text
|
||||
p.setPen(QColor(0xff, 0xff, 0xff));
|
||||
p.setRenderHint(QPainter::TextAntialiasing);
|
||||
p.setFont(topFont);
|
||||
if (alert.size == cereal::SelfdriveState::AlertSize::SMALL) {
|
||||
p.drawText(rect, Qt::AlignCenter | Qt::TextWordWrap, alert.text1);
|
||||
} else if (alert.size == cereal::SelfdriveState::AlertSize::MID) {
|
||||
QRect topText = QRect(rect.x(), rect.top() + margin, rect.width(), topTextBoundingRect.height());
|
||||
p.drawText(topText, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap | Qt::AlignCenter, alert.text1);
|
||||
p.setFont(bottomFont);
|
||||
p.drawText(QRect(rect.x(), topText.bottom() + margin, rect.width(), bottomTextBoundingRect.height()),
|
||||
Qt::AlignHCenter | Qt::TextWordWrap | Qt::AlignCenter, alert.text2);
|
||||
}
|
||||
}
|
||||
22
selfdrive/ui/sunnypilot/qt/onroad/alerts.h
Normal file
22
selfdrive/ui/sunnypilot/qt/onroad/alerts.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/qt/onroad/alerts.h"
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
|
||||
class OnroadAlertsSP : public OnroadAlerts {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OnroadAlertsSP(QWidget *parent = 0) : OnroadAlerts(parent) {}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
Alert getAlert(const SubMaster &sm, uint64_t started_frame);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ public:
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent* event) override;
|
||||
};
|
||||
|
||||
@@ -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,62 @@ 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();
|
||||
if (!s.scene.started) standstillElapsedTime = 0.0;
|
||||
|
||||
greenLightAlert = lp_sp.getE2eAlerts().getGreenLightAlert();
|
||||
leadDepartAlert = lp_sp.getE2eAlerts().getLeadDepartAlert();
|
||||
// override stock current speed values
|
||||
float v_ego = (v_ego_cluster_seen && !s.scene.trueVEgoUI) ? car_state.getVEgoCluster() : car_state.getVEgo();
|
||||
speed = std::max<float>(0.0f, v_ego * (is_metric ? MS_TO_KPH : MS_TO_MPH));
|
||||
hideVEgoUI = s.scene.hideVEgoUI;
|
||||
|
||||
leftBlinkerOn = car_state.getLeftBlinker();
|
||||
rightBlinkerOn = car_state.getRightBlinker();
|
||||
leftBlindspot = car_state.getLeftBlindspot();
|
||||
rightBlindspot = car_state.getRightBlindspot();
|
||||
|
||||
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) {
|
||||
@@ -128,6 +167,10 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
drawSetSpeedSP(p, surface_rect);
|
||||
}
|
||||
|
||||
if (!hideVEgoUI) {
|
||||
drawCurrentSpeedSP(p, surface_rect);
|
||||
}
|
||||
|
||||
if (!reversing) {
|
||||
// Smart Cruise Control
|
||||
int x_offset = -260;
|
||||
@@ -170,11 +213,6 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
|
||||
drawRightDevUI(p, surface_rect.right() - 184 - UI_BORDER_SIZE * 2, UI_BORDER_SIZE * 2 + rect_right.height());
|
||||
}
|
||||
|
||||
// Standstill Timer
|
||||
if (standstillTimer) {
|
||||
drawStandstillTimer(p, surface_rect.right() / 12 * 10, surface_rect.bottom() / 12 * 1.53);
|
||||
}
|
||||
|
||||
// Speed Limit
|
||||
bool showSpeedLimit;
|
||||
bool speed_limit_assist_pre_active_pulse = pulseElement(speedLimitAssistFrame);
|
||||
@@ -208,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();
|
||||
@@ -218,15 +256,34 @@ 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);
|
||||
} else {
|
||||
}
|
||||
// Standstill Timer
|
||||
else if (standstillTimer && isStandstill) {
|
||||
alert_img = QPixmap();
|
||||
|
||||
standstillElapsedTime += 1.0 / UI_FREQ;
|
||||
int minute = static_cast<int>(standstillElapsedTime / 60);
|
||||
int second = static_cast<int>(standstillElapsedTime - (minute * 60));
|
||||
alert_text = QString("%1:%2").arg(minute, 1, 10, QChar('0')).arg(second, 2, 10, QChar('0'));
|
||||
drawE2eAlert(p, surface_rect, tr("STOPPED"));
|
||||
e2eAlertFrame++;
|
||||
}
|
||||
// No Alerts displayed
|
||||
else {
|
||||
e2eAlertFrame = 0;
|
||||
if (!isStandstill) standstillElapsedTime = 0.0;
|
||||
}
|
||||
|
||||
// Blinker
|
||||
if (showTurnSignals) {
|
||||
drawBlinker(p, surface_rect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,40 +442,6 @@ void HudRendererSP::drawBottomDevUI(QPainter &p, int x, int y) {
|
||||
rw += drawBottomDevUIElement(p, rw, y, altitudeElement.value, altitudeElement.label, altitudeElement.units, altitudeElement.color);
|
||||
}
|
||||
|
||||
void HudRendererSP::drawStandstillTimer(QPainter &p, int x, int y) {
|
||||
if (isStandstill) {
|
||||
standstillElapsedTime += 1.0 / UI_FREQ;
|
||||
|
||||
int minute = static_cast<int>(standstillElapsedTime / 60);
|
||||
int second = static_cast<int>(standstillElapsedTime - (minute * 60));
|
||||
|
||||
// stop sign for standstill timer
|
||||
const int size = 190; // size
|
||||
const float angle = M_PI / 8.0;
|
||||
|
||||
QPolygon octagon;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
float curr_angle = angle + i * M_PI / 4.0;
|
||||
int point_x = x + size / 2 * cos(curr_angle);
|
||||
int point_y = y + size / 2 * sin(curr_angle);
|
||||
octagon << QPoint(point_x, point_y);
|
||||
}
|
||||
|
||||
p.setPen(QPen(Qt::white, 6));
|
||||
p.setBrush(QColor(255, 90, 81, 200)); // red pastel
|
||||
p.drawPolygon(octagon);
|
||||
|
||||
QString time_str = QString("%1:%2").arg(minute, 1, 10, QChar('0')).arg(second, 2, 10, QChar('0'));
|
||||
p.setFont(InterFont(55, QFont::Bold));
|
||||
p.setPen(Qt::white);
|
||||
QRect timerTextRect = p.fontMetrics().boundingRect(QString(time_str));
|
||||
timerTextRect.moveCenter({x, y});
|
||||
p.drawText(timerTextRect, Qt::AlignCenter, QString(time_str));
|
||||
} else {
|
||||
standstillElapsedTime = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void HudRendererSP::drawSpeedLimitSigns(QPainter &p, QRect &sign_rect) {
|
||||
bool speedLimitWarningEnabled = speedLimitMode >= SpeedLimitMode::WARNING; // TODO-SP: update to include SpeedLimitMode::ASSIST
|
||||
bool hasSpeedLimit = speedLimitValid || speedLimitLastValid;
|
||||
@@ -546,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;
|
||||
}
|
||||
@@ -640,7 +664,7 @@ void HudRendererSP::drawRoadName(QPainter &p, const QRect &surface_rect) {
|
||||
|
||||
void HudRendererSP::drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect) {
|
||||
const int sign_margin = 12;
|
||||
const int arrow_spacing = sign_margin * 3;
|
||||
const int arrow_spacing = sign_margin * 1.4;
|
||||
int arrow_x = sign_rect.right() + arrow_spacing;
|
||||
|
||||
int _set_speed = std::nearbyint(set_speed);
|
||||
@@ -689,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)) : "–";
|
||||
@@ -701,33 +738,150 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr);
|
||||
}
|
||||
|
||||
void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect) {
|
||||
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);
|
||||
void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text) {
|
||||
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 = pulseElement(e2eAlertFrame) ? QColor(255, 255, 255, 75) : QColor(0, 255, 0, 75);
|
||||
QColor frameColor;
|
||||
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 = pulseElement(e2eAlertFrame) ? QColor(255, 255, 255, 255) : QColor(0, 255, 0, 255);
|
||||
p.setFont(InterFont(48, QFont::Bold));
|
||||
QColor txtColor;
|
||||
QFont font;
|
||||
int alert_bottom_adjustment;
|
||||
if (!alert_alt_text.isEmpty()) {
|
||||
font = InterFont(100, QFont::Bold);
|
||||
alert_bottom_adjustment = 5;
|
||||
txtColor = QColor(255, 255, 255, 255);
|
||||
} else {
|
||||
font = InterFont(48, QFont::Bold);
|
||||
alert_bottom_adjustment = 7;
|
||||
txtColor = pulseElement(e2eAlertFrame) ? QColor(255, 255, 255, 255) : QColor(0, 255, 0, 190);
|
||||
}
|
||||
p.setPen(txtColor);
|
||||
p.setFont(font);
|
||||
QFontMetrics fm(p.font());
|
||||
QRect textRect = fm.boundingRect(alertRect, Qt::TextWordWrap, alert_text);
|
||||
textRect.moveCenter({alertRect.center().x(), alertRect.center().y()});
|
||||
textRect.moveBottom(alertRect.bottom() - alertRect.height() / 7);
|
||||
textRect.moveBottom(alertRect.bottom() - alertRect.height() / alert_bottom_adjustment);
|
||||
p.drawText(textRect, Qt::AlignCenter, alert_text);
|
||||
|
||||
// Alert Image
|
||||
QPointF pixmapCenterOffset = QPointF(alert_img.width() / 2.0, alert_img.height() / 2.0);
|
||||
QPointF drawPoint = center - pixmapCenterOffset;
|
||||
p.drawPixmap(drawPoint, alert_img);
|
||||
if (!alert_alt_text.isEmpty()) {
|
||||
// Alert Alternate Text
|
||||
p.setFont(InterFont(80, QFont::Bold));
|
||||
p.setPen(QColor(255, 175, 3, 240));
|
||||
QFontMetrics fmt(p.font());
|
||||
QRect topTextRect = fmt.boundingRect(alertRect, Qt::TextWordWrap, alert_alt_text);
|
||||
topTextRect.moveCenter({alertRect.center().x(), alertRect.center().y()});
|
||||
topTextRect.moveTop(alertRect.top() + alertRect.height() / 3.5);
|
||||
p.drawText(topTextRect, Qt::AlignCenter, alert_alt_text);
|
||||
} else {
|
||||
// Alert Image instead of Top Text
|
||||
QPointF pixmapCenterOffset = QPointF(alert_img.width() / 2.0, alert_img.height() / 2.0);
|
||||
QPointF drawPoint = center - pixmapCenterOffset;
|
||||
p.drawPixmap(drawPoint, alert_img);
|
||||
}
|
||||
}
|
||||
|
||||
void HudRendererSP::drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect) {
|
||||
QString speedStr = QString::number(std::nearbyint(speed));
|
||||
|
||||
p.setFont(InterFont(176, QFont::Bold));
|
||||
HudRenderer::drawText(p, surface_rect.center().x(), 210, speedStr);
|
||||
|
||||
p.setFont(InterFont(66));
|
||||
HudRenderer::drawText(p, surface_rect.center().x(), 290, is_metric ? tr("km/h") : tr("mph"), 200);
|
||||
}
|
||||
|
||||
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 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 QPen bgBorder(Qt::white, 5);
|
||||
const QPen arrowPen(Qt::NoPen);
|
||||
|
||||
p.save();
|
||||
|
||||
auto drawArrow = [&](int cx, int cy, int dir, const QBrush &arrowBrush) {
|
||||
const int bodyLength = arrowLength / 2;
|
||||
const int bodyWidth = arrowLength / 2;
|
||||
const int headLength = arrowLength / 2;
|
||||
const int headWidth = arrowLength;
|
||||
|
||||
QPolygon arrow;
|
||||
arrow.reserve(7);
|
||||
arrow << QPoint(cx - dir * bodyLength, cy - bodyWidth / 2)
|
||||
<< QPoint(cx, cy - bodyWidth / 2)
|
||||
<< QPoint(cx, cy - headWidth / 2)
|
||||
<< QPoint(cx + dir * headLength, cy)
|
||||
<< QPoint(cx, cy + headWidth / 2)
|
||||
<< QPoint(cx, cy + bodyWidth / 2)
|
||||
<< QPoint(cx - dir * bodyLength, cy + bodyWidth / 2);
|
||||
|
||||
p.setPen(arrowPen);
|
||||
p.setBrush(arrowBrush);
|
||||
p.drawPolygon(arrow);
|
||||
};
|
||||
|
||||
auto drawCircle = [&](int cx, int cy, const QBrush &bgBrush) {
|
||||
p.setPen(bgBorder);
|
||||
p.setBrush(bgBrush);
|
||||
p.drawEllipse(QPoint(cx, cy), circleRadius, circleRadius);
|
||||
};
|
||||
|
||||
struct BlinkerSide { bool on; int dir; bool blocked; int cx; };
|
||||
const std::array<BlinkerSide, 2> sides = {{
|
||||
{leftBlinkerOn, -1, hazard ? true : (leftBlinkerOn && leftBlindspot), centerX - x_gap},
|
||||
{rightBlinkerOn, 1, hazard ? true : (rightBlinkerOn && rightBlindspot), centerX + x_gap},
|
||||
}};
|
||||
|
||||
for (const auto &s: sides) {
|
||||
if (!s.on) continue;
|
||||
|
||||
QColor bgColor = s.blocked ? QColor(135, 23, 23) : QColor(23, 134, 68);
|
||||
QColor arrowColor = s.blocked ? QColor(66, 12, 12) : QColor(12, 67, 34);
|
||||
if (pulseElement(blinkerFrameCounter)) arrowColor = Qt::white;
|
||||
|
||||
const QBrush bgBrush(bgColor);
|
||||
const QBrush arrowBrush(arrowColor);
|
||||
|
||||
drawCircle(s.cx, y_offset, bgBrush);
|
||||
drawArrow(s.cx, y_offset, s.dir, arrowBrush);
|
||||
}
|
||||
|
||||
p.restore();
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ private:
|
||||
void drawRoadName(QPainter &p, const QRect &surface_rect);
|
||||
void drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect);
|
||||
void drawSetSpeedSP(QPainter &p, const QRect &surface_rect);
|
||||
void drawE2eAlert(QPainter &p, const QRect &surface_rect);
|
||||
void drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text = "");
|
||||
void drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect);
|
||||
void drawBlinker(QPainter &p, const QRect &surface_rect);
|
||||
|
||||
bool lead_status;
|
||||
float lead_d_rel;
|
||||
@@ -81,6 +83,7 @@ private:
|
||||
bool speedLimitValid;
|
||||
bool speedLimitLastValid;
|
||||
float speedLimitFinalLast;
|
||||
cereal::LongitudinalPlanSP::SpeedLimit::Source speedLimitSource;
|
||||
bool speedLimitAheadValid;
|
||||
float speedLimitAhead;
|
||||
float speedLimitAheadDistance;
|
||||
@@ -94,16 +97,27 @@ 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;
|
||||
bool leftBlinkerOn;
|
||||
bool rightBlinkerOn;
|
||||
bool leftBlindspot;
|
||||
bool rightBlindspot;
|
||||
int blinkerFrameCounter;
|
||||
int lastBlinkerStatus;
|
||||
bool showTurnSignals;
|
||||
|
||||
bool carControlEnabled;
|
||||
float speedCluster = 0;
|
||||
int icbm_active_counter = 0;
|
||||
bool pcmCruiseSpeed = true;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -95,13 +95,12 @@ QStringList searchFromList(const QString &query, const QStringList &list) {
|
||||
return list;
|
||||
}
|
||||
|
||||
QStringList search_terms = query.simplified().toLower().split(" ", QString::SkipEmptyParts);
|
||||
QStringList search_terms = query.simplified().toLower().replace(QRegularExpression("[^a-zA-Z0-9\\s]"), " ").split(" ", QString::SkipEmptyParts);
|
||||
QStringList search_results;
|
||||
|
||||
for (const QString &element : list) {
|
||||
if (std::all_of(search_terms.begin(), search_terms.end(), [&](const QString &term) {
|
||||
QString normalized_term = term.normalized(QString::NormalizationForm_KD).toLower();
|
||||
normalized_term.remove(QRegularExpression("[^a-zA-Z0-9\\s]"));
|
||||
QString normalized_element = element.normalized(QString::NormalizationForm_KD).toLower();
|
||||
return normalized_element.contains(normalized_term, Qt::CaseInsensitive);
|
||||
})) {
|
||||
|
||||
@@ -545,10 +545,8 @@ public:
|
||||
main_layout->removeWidget(title_label);
|
||||
hlayout->addWidget(title_label, 1);
|
||||
}
|
||||
if (spacingItem != nullptr && main_layout->indexOf(spacingItem) != -1) {
|
||||
main_layout->removeItem(spacingItem);
|
||||
spacingItem = nullptr;
|
||||
}
|
||||
main_layout->removeItem(spacingItem);
|
||||
spacingItem = nullptr;
|
||||
}
|
||||
|
||||
label.setStyleSheet(label_enabled_style);
|
||||
|
||||
@@ -14,10 +14,10 @@ ExpandableToggleRow::ExpandableToggleRow(const QString ¶m, const QString &ti
|
||||
QObject::connect(this, &ExpandableToggleRow::toggleFlipped, this, &ExpandableToggleRow::toggleClicked);
|
||||
|
||||
collapsibleWidget = new QFrame(this);
|
||||
collapsibleWidget->setContentsMargins(0, 0, 0, 0);
|
||||
collapsibleWidget->setVisible(false);
|
||||
QVBoxLayout *collapsible_layout = new QVBoxLayout();
|
||||
collapsibleWidget->setLayout(collapsible_layout);
|
||||
collapsible_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
list = new ListWidgetSP(this, false);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) {
|
||||
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan",
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters",
|
||||
"carStateSP", "liveParameters", "liveMapDataSP"
|
||||
"carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP"
|
||||
});
|
||||
|
||||
// update timer
|
||||
@@ -64,20 +64,29 @@ void ui_update_params_sp(UIStateSP *s) {
|
||||
s->scene.standstill_timer = params.getBool("StandstillTimer");
|
||||
s->scene.speed_limit_mode = std::atoi(params.get("SpeedLimitMode").c_str());
|
||||
s->scene.road_name = params.getBool("RoadNameToggle");
|
||||
s->scene.trueVEgoUI = params.getBool("TrueVEgoUI");
|
||||
s->scene.hideVEgoUI = params.getBool("HideVEgoUI");
|
||||
|
||||
// Onroad Screen Brightness
|
||||
s->scene.onroadScreenOffBrightness = std::atoi(params.get("OnroadScreenOffBrightness").c_str());
|
||||
s->scene.onroadScreenOffControl = params.getBool("OnroadScreenOffControl");
|
||||
s->scene.onroadScreenOffTimerParam = std::atoi(params.get("OnroadScreenOffTimer").c_str());
|
||||
s->reset_onroad_sleep_timer();
|
||||
|
||||
s->scene.turn_signals = params.getBool("ShowTurnSignals");
|
||||
s->scene.chevron_info = std::atoi(params.get("ChevronInfo").c_str());
|
||||
s->scene.blindspot_ui = params.getBool("BlindSpot");
|
||||
s->scene.rainbow_mode = params.getBool("RainbowMode");
|
||||
}
|
||||
|
||||
void UIStateSP::reset_onroad_sleep_timer() {
|
||||
if (scene.onroadScreenOffTimerParam >= 0 and scene.onroadScreenOffControl) {
|
||||
scene.onroadScreenOffTimer = scene.onroadScreenOffTimerParam * UI_FREQ;
|
||||
} else {
|
||||
void UIStateSP::reset_onroad_sleep_timer(OnroadTimerStatusToggle toggleTimerStatus) {
|
||||
// Toggling from active state to inactive
|
||||
if (toggleTimerStatus == OnroadTimerStatusToggle::PAUSE and scene.onroadScreenOffTimer != -1) {
|
||||
scene.onroadScreenOffTimer = -1;
|
||||
}
|
||||
// Toggling from a previously inactive state or resetting an active timer
|
||||
else if ((scene.onroadScreenOffTimerParam >= 0 and scene.onroadScreenOffControl and scene.onroadScreenOffTimer != -1) or toggleTimerStatus == OnroadTimerStatusToggle::RESUME) {
|
||||
scene.onroadScreenOffTimer = scene.onroadScreenOffTimerParam * UI_FREQ;
|
||||
}
|
||||
}
|
||||
|
||||
DeviceSP::DeviceSP(QObject *parent) : Device(parent) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user