Compare commits

..

41 Commits

Author SHA1 Message Date
Jason Wen c1e15e5544 changelog: add new contributor entry 2025-10-25 01:00:46 -04:00
MuskratGG 3a45fff1b9 ui: openpilot Longitudinal Control → sunnypilot Longitudinal Control (#1422)
* Update developer_panel.cc

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

* Update warning message for longitudinal control

* more

* a bit more

* slightly more

---------

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

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

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

* split and rename

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

* add test

* unused

* use constant

* eh

* flip

* universal it

* check release state and align in tests

* use this

* eh

* update changelog

* Speed Limit Assist: Disable for Rivian

* desc

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

* add test

* unused

* use constant

* eh

* flip

* universal it

* check release state and align in tests

* use this

* eh

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

* a bit more

* in another PR

* in another PR

* since when?

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

* car -> platform

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

* changelog

* singular

* show ours

* actual

* readjust

* updated

* more

* official spelling

* more

* sync

* fix

* send it

* push

* we never had this lol

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

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

* more remotes for legacy support

* bruh

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

* oops

* oops

* single location

* some more

* fix cruise toggles

* always init true

* check this

* nah

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

* ok no more for non pcm long it was annoying

* 5 seconds preActive for non pcm long now

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

This reverts commit dfcc601035.

* dynamic alert size

* do the same here

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

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

* not used

* rename

* 10 frames for both

* time based

* magic

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

* less

* shorter trigger

* lol

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

* Apply suggestion from @sunnyhaibin

* sir, this is Wendy's

* this is still a Wendy's

---------

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

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

* fix

* logging and no DEC when no long

* ui

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

This reverts commit 339bc0b8b3.

* cache it

* format

---------

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

This reverts commit 7229c7541e.

* soundd: custom audible alerts

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

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

* bump

* bump

* fix

* bump

* fix init

* wat

* use just standstill for now

* Revert "use just standstill for now"

This reverts commit f72cce6892.

* bump

* bump

* fix it

* only send at 10

* bump

* fix type

* forget about planner resume, it sucks

* try to send off_accel

* still need it

* always send

* disable safety checks for now

* same

* more

* all the time for both

* don't need i guess

* bump

* try 15 frames per try

* all should have it

* try 3 for all

* use throttle for all preglobal?

* bump

* bump

* separate thresholds between preglobal and global

* longer wait before sending

* shorter time but immediately resend

* quick

* new timeout

* about to cry

* same thing but another try

* no need

* round 3

* try 1.4

* lower!

* 1.2

* last try

* beta asf

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

* match

* less

* huh?

* unused

---------

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

* lint

* inline

* fix access

* actually fix the param

* inherit them

* more lint
2025-10-13 19:37:58 -04:00
82 changed files with 2122 additions and 1113 deletions
+1
View File
@@ -3,3 +3,4 @@ REGIST
PullRequest
cancelled
FOF
NoO
@@ -79,7 +79,7 @@ jobs:
is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')";
echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT
stable_version=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g');
stable_version=$(cat sunnypilot/common/version.h | grep SUNNYPILOT_VERSION | sed -e 's/[^0-9|.]//g');
echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT
echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT
fi
+1077
View File
File diff suppressed because it is too large Load Diff
+47 -11
View File
@@ -69,6 +69,48 @@ struct LeadData {
struct SelfdriveStateSP @0x81c2f05a394cf4af {
mads @0 :ModularAssistiveDrivingSystem;
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
enum AudibleAlert {
none @0;
engage @1;
disengage @2;
refuse @3;
warningSoft @4;
warningImmediate @5;
prompt @6;
promptRepeat @7;
promptDistracted @8;
# unused, these are reserved for upstream events so we don't collide
reserved9 @9;
reserved10 @10;
reserved11 @11;
reserved12 @12;
reserved13 @13;
reserved14 @14;
reserved15 @15;
reserved16 @16;
reserved17 @17;
reserved18 @18;
reserved19 @19;
reserved20 @20;
reserved21 @21;
reserved22 @22;
reserved23 @23;
reserved24 @24;
reserved25 @25;
reserved26 @26;
reserved27 @27;
reserved28 @28;
reserved29 @29;
reserved30 @30;
promptSingleLow @31;
promptSingleHigh @32;
}
}
struct ModelManagerSP @0xaedffd8f31e7b55d {
@@ -150,7 +192,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
aTarget @5 :Float32;
events @6 :List(OnroadEventSP.Event);
e2eAlerts @7 :E2eAlerts;
accelPersonality @8 :AccelerationPersonality;
struct DynamicExperimentalControl {
state @0 :DynamicExperimentalControlState;
@@ -253,11 +294,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
greenLightAlert @0 :Bool;
leadDepartAlert @1 :Bool;
}
enum AccelerationPersonality {
sport @0;
normal @1;
eco @2;
}
}
struct OnroadEventSP @0xda96579883444c35 {
@@ -410,12 +446,12 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
}
enum TurnDirection {
none @0;
turnLeft @1;
turnRight @2;
enum TurnDirection {
none @0;
turnLeft @1;
turnRight @2;
}
}
struct CustomReserved10 @0xcb9fd56c7057593a {
+5 -6
View File
@@ -130,7 +130,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
@@ -155,15 +154,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
{"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}},
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "100"}},
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
{"OnroadScreenOffControl", {PERSISTENT | BACKUP, BOOL}},
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "0"}},
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -172,9 +172,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
{"VibePersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"VibeAccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"VibeFollowPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
// MADS params
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
@@ -209,6 +206,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"}},
+3 -1
View File
@@ -15,6 +15,8 @@
#include "common/version.h"
#include "system/hardware/hw.h"
#include "sunnypilot/common/version.h"
class SwaglogState {
public:
SwaglogState() {
@@ -56,7 +58,7 @@ public:
if (char* daemon_name = getenv("MANAGER_DAEMON")) {
ctx_j["daemon"] = daemon_name;
}
ctx_j["version"] = COMMA_VERSION;
ctx_j["version"] = SUNNYPILOT_VERSION;
ctx_j["dirty"] = !getenv("CLEAN");
ctx_j["device"] = Hardware::get_name();
}
+1 -1
View File
@@ -6,7 +6,7 @@ from openpilot.common.markdown import parse_markdown
class TestMarkdown:
def test_all_release_notes(self):
with open(os.path.join(BASEDIR, "RELEASES.md")) as f:
with open(os.path.join(BASEDIR, "CHANGELOG.md")) as f:
release_notes = f.read().split("\n\n")
assert len(release_notes) > 10
+3 -1
View File
@@ -9,6 +9,8 @@
#include "system/hardware/hw.h"
#include "third_party/json11/json11.hpp"
#include "sunnypilot/common/version.h"
std::string daemon_name = "testy";
std::string dongle_id = "test_dongle_id";
int LINE_NO = 0;
@@ -53,7 +55,7 @@ void recv_log(int thread_cnt, int thread_msg_cnt) {
REQUIRE(ctx["dongle_id"].string_value() == dongle_id);
REQUIRE(ctx["dirty"].bool_value() == true);
REQUIRE(ctx["version"].string_value() == COMMA_VERSION);
REQUIRE(ctx["version"].string_value() == SUNNYPILOT_VERSION);
std::string device = Hardware::get_name();
REQUIRE(ctx["device"].string_value() == device);
+16 -14
View File
@@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 337 Supported Cars
# 339 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@@ -21,7 +21,10 @@ A supported vehicle is one that just works when you install a comma device. All
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2017">Buy Here</a></sub></details>|||
|Chevrolet|Bolt EV Non-ACC 2018-21|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2018-21">Buy Here</a></sub></details>|||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Equinox 2019-22">Buy Here</a></sub></details>|||
|Chevrolet|Malibu Non-ACC 2016-23|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Malibu Non-ACC 2016-23">Buy Here</a></sub></details>|||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Silverado 1500 2020-21">Buy Here</a></sub></details>|||
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Trailblazer 2021-22">Buy Here</a></sub></details>|||
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica 2017-18">Buy Here</a></sub></details>|||
@@ -236,20 +239,20 @@ A supported vehicle is one that just works when you install a comma device. All
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Rivian A connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|Subaru|Ascent 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Ascent 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2019-21|All[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2020-22|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2020-22|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|openpilot available[<sup>1,9</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Škoda|Fabia 2022-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Škoda|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
@@ -308,7 +311,6 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Toyota|Sienna 2018-20|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Toyota|Wildlander PHEV 2021|All|openpilot|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Wildlander PHEV 2021">Buy Here</a></sub></details>|||
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
+1 -1
View File
@@ -39,7 +39,7 @@ cd $BUILD_DIR
rm -f panda/board/obj/panda.bin.signed
rm -f panda/board/obj/panda_h7.bin.signed
VERSION=$(cat common/version.h | awk -F[\"-] '{print $2}')
VERSION=$(cat sunnypilot/common/version.h | awk -F[\"-] '{print $2}')
echo "[-] committing version $VERSION T=$SECONDS"
git add -f .
git commit -a -m "openpilot v$VERSION release"
+1 -1
View File
@@ -49,7 +49,7 @@ rm -f panda/board/obj/panda.bin.signed
GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD)
GIT_COMMIT_DATE=$(git --git-dir=$SOURCE_DIR/.git show --no-patch --format='%ct %ci' HEAD)
DATETIME=$(date '+%Y-%m-%dT%H:%M:%S')
VERSION=$(cat $SOURCE_DIR/common/version.h | awk -F\" '{print $2}')
VERSION=$(cat $SOURCE_DIR/sunnypilot/common/version.h | awk -F\" '{print $2}')
echo -n "$GIT_HASH" > git_src_commit
echo -n "$GIT_COMMIT_DATE" > git_src_commit_date
+2 -2
View File
@@ -30,7 +30,7 @@ if [ -z "$GIT_ORIGIN" ]; then
fi
# "Tagging"
echo "#define COMMA_VERSION \"$VERSION\"" > ${OUTPUT_DIR}/common/version.h
echo "#define SUNNYPILOT_VERSION \"$VERSION\"" > ${OUTPUT_DIR}/sunnypilot/common/version.h
## set git identity
#source $DIR/identity.sh
@@ -55,7 +55,7 @@ git add -f .
# include source commit hash and build date in commit
GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD)
DATETIME=$(date '+%Y-%m-%dT%H:%M:%S')
SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/common/version.h)
SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/sunnypilot/common/version.h)
# Commit with detailed message
git commit -a -m "sunnypilot v$VERSION
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dbfa5858c0a672411ffdc691efdecb06d01ae458cc1df409bcf3fdeaa4756f72
size 34638
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db9671bb03e01f119bba1eb6cc0507e0f039ac4e5b7f9f839a87071c52e86e56
size 44416
+2 -1
View File
@@ -88,6 +88,7 @@ class Car:
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
is_release = self.params.get_bool("IsReleaseBranch")
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
if CI is None:
# wait for one pandaState and one CAN packet
@@ -110,7 +111,7 @@ class Car:
init_params_list_sp = sunnypilot_interfaces.initialize_params(self.params)
self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params,
fixed_fingerprint, init_params_list_sp)
fixed_fingerprint, init_params_list_sp, is_release_sp)
sunnypilot_interfaces.setup_interfaces(self.CI, self.params)
self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP, self.CI.CP_SP)
self.CP = self.CI.CP
+1 -1
View File
@@ -151,7 +151,7 @@ class TestCarModelBase(unittest.TestCase):
cls.CarInterface = interfaces[cls.platform]
cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, alpha_long, False, docs=False)
cls.CP_SP = cls.CarInterface.get_params_sp(cls.CP, cls.platform, cls.fingerprint, car_fw, alpha_long, docs=False)
cls.CP_SP = cls.CarInterface.get_params_sp(cls.CP, cls.platform, cls.fingerprint, car_fw, alpha_long, False, docs=False)
assert cls.CP
assert cls.CP_SP
assert cls.CP.carFingerprint == cls.platform
+3 -1
View File
@@ -99,7 +99,6 @@ class Controls(ControlsExt, ModelStateBase):
self.LaC.extension.update_model_v2(self.sm['modelV2'])
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
self.LaC.extension.update_lateral_lag(self.lat_delay)
long_plan = self.sm['longitudinalPlan']
@@ -234,6 +233,9 @@ class Controls(ControlsExt, ModelStateBase):
while not evt.is_set():
self.get_params_sp()
if self.CP.lateralTuning.which() == 'torque':
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
time.sleep(0.1)
def run(self):
+6 -5
View File
@@ -6,6 +6,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTur
LaneChangeState = log.LaneChangeState
LaneChangeDirection = log.LaneChangeDirection
TurnDirection = custom.ModelDataV2SP.TurnDirection
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
LANE_CHANGE_TIME_MAX = 10.
@@ -32,9 +33,9 @@ DESIRES = {
}
TURN_DESIRES = {
custom.TurnDirection.none: log.Desire.none,
custom.TurnDirection.turnLeft: log.Desire.turnLeft,
custom.TurnDirection.turnRight: log.Desire.turnRight,
TurnDirection.none: log.Desire.none,
TurnDirection.turnLeft: log.Desire.turnLeft,
TurnDirection.turnRight: log.Desire.turnRight,
}
@@ -49,7 +50,7 @@ class DesireHelper:
self.desire = log.Desire.none
self.alc = AutoLaneChangeController(self)
self.lane_turn_controller = LaneTurnController(self)
self.lane_turn_direction = custom.TurnDirection.none
self.lane_turn_direction = TurnDirection.none
@staticmethod
def get_lane_change_direction(CS):
@@ -126,7 +127,7 @@ class DesireHelper:
self.prev_one_blinker = one_blinker
if self.lane_turn_direction != custom.TurnDirection.none:
if self.lane_turn_direction != TurnDirection.none:
self.desire = TURN_DESIRES[self.lane_turn_direction]
else:
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]
@@ -10,8 +10,6 @@ from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.modeld.constants import index_function
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
from openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.vibe_personality import VibePersonalityController
if __name__ == '__main__': # generating code
from openpilot.third_party.acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
else:
@@ -230,7 +228,6 @@ class LongitudinalMpc:
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.reset()
self.source = SOURCES[2]
self.vibe_controller = VibePersonalityController()
def reset(self):
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
@@ -331,18 +328,10 @@ class LongitudinalMpc:
return lead_xv
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard):
t_follow = get_T_FOLLOW(personality)
v_ego = self.x0[1]
# Get following distance
t_follow_vibe = self.vibe_controller.get_follow_distance_multiplier(v_ego)
t_follow = t_follow_vibe if t_follow_vibe is not None else get_T_FOLLOW(personality)
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
# Get acceleration limits
accel_limits = self.vibe_controller.get_accel_limits(v_ego)
a_cruise_min = accel_limits[0] if accel_limits is not None else CRUISE_MIN_ACCEL
lead_xv_0 = self.process_lead(radarstate.leadOne)
lead_xv_1 = self.process_lead(radarstate.leadTwo)
@@ -361,7 +350,7 @@ class LongitudinalMpc:
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
# when the leads are no factor.
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
# TODO does this make sense when max_a is negative?
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
@@ -416,7 +405,7 @@ class LongitudinalMpc:
if any((lead_0_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0):
self.source = 'lead0'
if any((lead_1_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0) and \
(lead_1_obstacle[0] - lead_0_obstacle[0]):
(lead_1_obstacle[0] - lead_0_obstacle[0]):
self.source = 'lead1'
def run(self):
@@ -51,12 +51,12 @@ def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
class LongitudinalPlanner(LongitudinalPlannerSP):
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
def __init__(self, CP, CP_SP, init_v=0.0, init_a=0.0, dt=DT_MDL):
self.CP = CP
self.mpc = LongitudinalMpc(dt=dt)
# TODO remove mpc modes when TR released
self.mpc.mode = 'acc'
LongitudinalPlannerSP.__init__(self, self.CP, self.mpc)
LongitudinalPlannerSP.__init__(self, self.CP, CP_SP, self.mpc)
self.fcw = False
self.dt = dt
self.allow_throttle = True
@@ -124,11 +124,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
if mode == 'acc':
accel_limits = self.vibe_controller.get_accel_limits(v_ego)
if accel_limits is not None:
accel_clip = [ACCEL_MIN, accel_limits[1]]
else:
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
else:
+6 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
from cereal import car
from cereal import car, custom
from openpilot.common.gps import get_gps_location_service
from openpilot.common.params import Params
from openpilot.common.realtime import Priority, config_realtime_process
@@ -17,10 +17,14 @@ def main():
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
cloudlog.info("plannerd got CarParams: %s", CP.brand)
cloudlog.info("plannerd is waiting for CarParamsSP")
CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP)
cloudlog.info("plannerd got CarParamsSP")
gps_location_service = get_gps_location_service(params)
ldw = LaneDepartureWarning()
longitudinal_planner = LongitudinalPlanner(CP)
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
'liveMapDataSP', 'carStateSP', gps_location_service],
+10 -3
View File
@@ -24,6 +24,7 @@ from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroa
from openpilot.system.version import get_build_metadata
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
from openpilot.sunnypilot import get_sanitize_int_param
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
@@ -43,6 +44,7 @@ LaneChangeDirection = log.LaneChangeDirection
EventName = log.OnroadEvent.EventName
ButtonType = car.CarState.ButtonEvent.Type
SafetyModel = car.CarParams.SafetyModel
TurnDirection = custom.ModelDataV2SP.TurnDirection
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
@@ -130,7 +132,12 @@ class SelfdriveD(CruiseHelper):
self.logged_comm_issue = None
self.not_running_prev = None
self.experimental_mode = False
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
self.personality = get_sanitize_int_param(
"LongitudinalPersonality",
min(log.LongitudinalPersonality.schema.enumerants.values()),
max(log.LongitudinalPersonality.schema.enumerants.values()),
self.params
)
self.recalibrating_seen = False
self.state_machine = StateMachine()
self.rk = Ratekeeper(100, print_delay_threshold=None)
@@ -299,9 +306,9 @@ class SelfdriveD(CruiseHelper):
# Handle lane turn
lane_turn_direction = self.sm['modelDataV2SP'].laneTurnDirection
if lane_turn_direction == custom.TurnDirection.turnLeft:
if lane_turn_direction == TurnDirection.turnLeft:
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnLeft)
elif lane_turn_direction == custom.TurnDirection.turnRight:
elif lane_turn_direction == TurnDirection.turnRight:
self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnRight)
for i, pandaState in enumerate(self.sm['pandaStates']):
@@ -51,7 +51,9 @@ class Plant:
from opendbc.car.honda.values import CAR
from opendbc.car.honda.interface import CarInterface
self.planner = LongitudinalPlanner(CarInterface.get_non_essential_params(CAR.HONDA_CIVIC), init_v=self.speed)
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
CP_SP = CarInterface.get_non_essential_params_sp(CP, CAR.HONDA_CIVIC)
self.planner = LongitudinalPlanner(CP, CP_SP, init_v=self.speed)
@property
def current_time(self):
+4 -4
View File
@@ -32,11 +32,11 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
experimentalLongitudinalToggle = new ParamControl(
"AlphaLongitudinalEnabled",
tr("openpilot Longitudinal Control (Alpha)"),
tr("sunnypilot Longitudinal Control (Alpha)"),
QString("<b>%1</b><br><br>%2")
.arg(tr("WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB)."))
.arg(tr("On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. "
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.")),
.arg(tr("WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB)."))
.arg(tr("On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. "
"Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha.")),
""
);
experimentalLongitudinalToggle->setConfirmation(true, false);
+1 -1
View File
@@ -12,7 +12,7 @@ public:
explicit DeveloperPanel(SettingsWindow *parent);
void showEvent(QShowEvent *event) override;
private:
protected:
Params params;
ParamControl* adbToggle;
ParamControl* joystickToggle;
+2 -27
View File
@@ -33,13 +33,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
"../assets/icons/experimental_white.svg",
false,
},
{
"DynamicExperimentalControl",
tr("Enable Dynamic Experimental Control"),
tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."),
"../assets/offroad/icon_blank.png",
false,
},
{
"DisengageOnAccelerator",
tr("Disengage on Accelerator Pedal"),
@@ -92,15 +85,7 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
"your steering wheel distance button."),
"../assets/icons/speed_limit.png",
longi_button_texts);
// accel controller
std::vector<QString> accel_personality_texts{tr("Sport"), tr("Normal"), tr("Eco")};
accel_personality_setting = new ButtonParamControlSP("AccelPersonality", tr("Acceleration Personality"),
tr("Normal is recommended. In sport mode, sunnypilot will provide aggressive acceleration for a dynamic driving experience. "
"In eco mode, sunnypilot will apply smoother and more relaxed acceleration. On supported cars, you can cycle through these "
"acceleration personality within Onroad Settings on the driving screen."),
"",
accel_personality_texts);
accel_personality_setting->showDescription();
// set up uiState update for personality setting
QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState);
@@ -128,7 +113,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
// insert longitudinal personality after NDOG toggle
if (param == "DisengageOnAccelerator") {
addItem(long_personality_setting);
addItem(accel_personality_setting);
}
}
@@ -149,13 +133,6 @@ void TogglesPanel::updateState(const UIState &s) {
}
uiState()->scene.personality = personality;
}
if (sm.updated("longitudinalPlanSP")) {
auto accel_personality = sm["longitudinalPlanSP"].getLongitudinalPlanSP().getAccelPersonality();
if (accel_personality != s.scene.accel_personality && s.scene.started && isVisible()) {
accel_personality_setting->setCheckedButton(static_cast<int>(accel_personality));
}
uiState()->scene.accel_personality = accel_personality;
}
}
void TogglesPanel::expandToggleDescription(const QString &param) {
@@ -202,18 +179,16 @@ void TogglesPanel::updateToggles() {
experimental_mode_toggle->setEnabled(true);
experimental_mode_toggle->setDescription(e2e_description);
long_personality_setting->setEnabled(true);
accel_personality_setting->setEnabled(true);
} else {
// no long for now
experimental_mode_toggle->setEnabled(false);
long_personality_setting->setEnabled(false);
accel_personality_setting->setEnabled(true);
params.remove("ExperimentalMode");
const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.");
QString long_desc = unavailable + " " + \
tr("openpilot longitudinal control may come in a future update.");
tr("sunnypilot longitudinal control may come in a future update.");
if (CP.getAlphaLongitudinalAvailable()) {
if (is_release) {
long_desc = unavailable + " " + tr("An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches.");
-1
View File
@@ -88,7 +88,6 @@ protected:
Params params;
std::map<std::string, ParamControl*> toggles;
ButtonParamControl *long_personality_setting;
ButtonParamControl *accel_personality_setting;
virtual void updateToggles();
};
+1 -2
View File
@@ -22,7 +22,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
update_model(model, lead_one);
drawLaneLines(painter);
drawPath(painter, model, surface_rect);
drawPath(painter, model, surface_rect.height());
if (longitudinal_control && sm.alive("radarState")) {
update_leads(radar_state, model.getPosition());
@@ -173,7 +173,6 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float
(1 - t) * start.alphaF() + t * end.alphaF());
}
void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &vd, const QRect &surface_rect) {
const float speedBuff = 10.;
-4
View File
@@ -39,9 +39,6 @@ protected:
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
void drawLaneLines(QPainter &painter);
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height);
virtual void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) {;
drawPath(painter, model, surface_rect.height());
}
void updatePathGradient(QLinearGradient &bg);
QColor blendColors(const QColor &start, const QColor &end, float t);
@@ -58,5 +55,4 @@ protected:
QPointF lead_vertices[2] = {};
Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero();
QRectF clip_region;
};
+10 -1
View File
@@ -4,7 +4,7 @@ import time
import wave
from cereal import car, messaging
from cereal import car, messaging, custom
from openpilot.common.basedir import BASEDIR
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import Ratekeeper
@@ -26,8 +26,15 @@ AMBIENT_DB = 30 # DB where MIN_VOLUME is applied
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
sound_list_sp: dict[int, tuple[str, int | None, float]] = {
# AudibleAlertSP, file name, play count (none for infinite)
AudibleAlertSP.promptSingleLow: ("prompt_single_low.wav", 1, MAX_VOLUME),
AudibleAlertSP.promptSingleHigh: ("prompt_single_high.wav", 1, MAX_VOLUME),
}
sound_list: dict[int, tuple[str, int | None, float]] = {
# AudibleAlert, file name, play count (none for infinite)
AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME),
@@ -40,6 +47,8 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME),
AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME),
**sound_list_sp,
}
def check_selfdrive_timeout_alert(sm):
@@ -60,7 +60,7 @@ DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(pare
void DeveloperPanelSP::updateToggles(bool offroad) {
bool disable_updates = params.getBool("DisableUpdates");
bool is_release = params.getBool("IsReleaseBranch");
bool is_release = params.getBool("IsReleaseBranch") || params.getBool("IsReleaseSpBranch");
bool is_tested = params.getBool("IsTestedBranch");
bool is_development = params.getBool("IsDevelopmentBranch");
@@ -79,6 +79,9 @@ void DeveloperPanelSP::updateToggles(bool offroad) {
enableGithubRunner->setVisible(!is_release);
errorLogBtn->setVisible(!is_release);
showAdvancedControls->setEnabled(true);
joystickToggle->setVisible(!is_release);
longManeuverToggle->setVisible(!is_release);
}
void DeveloperPanelSP::showEvent(QShowEvent *event) {
@@ -29,7 +29,7 @@ OnroadScreenBrightnessControl::OnroadScreenBrightnessControl(const QString &para
"Onroad Brightness",
"",
"",
{0, 100}, 10, true);
{0, 90}, 10, true);
connect(onroadScreenOffTimer, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);
connect(onroadScreenBrightness, &OptionControlSP::updateLabels, this, &OnroadScreenBrightnessControl::refresh);
@@ -13,9 +13,9 @@ enum class SpeedLimitOffsetType {
};
inline const QString SpeedLimitOffsetTypeTexts[]{
QObject::tr("None"),
QObject::tr("Fixed"),
QObject::tr("Percent"),
QT_TRANSLATE_NOOP("SpeedLimitSettings", "None"),
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Fixed"),
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Percent"),
};
enum class SpeedLimitSourcePolicy {
@@ -27,11 +27,11 @@ enum class SpeedLimitSourcePolicy {
};
inline const QString SpeedLimitSourcePolicyTexts[]{
QObject::tr("Car\nOnly"),
QObject::tr("Map\nOnly"),
QObject::tr("Car\nFirst"),
QObject::tr("Map\nFirst"),
QObject::tr("Combined\nData")
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nOnly"),
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nOnly"),
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Car\nFirst"),
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Map\nFirst"),
QT_TRANSLATE_NOOP("SpeedLimitPolicy", "Combined\nData")
};
enum class SpeedLimitMode {
@@ -42,8 +42,8 @@ enum class SpeedLimitMode {
};
inline const QString SpeedLimitModeTexts[]{
QObject::tr("Off"),
QObject::tr("Information"),
QObject::tr("Warning"),
QObject::tr("Assist"),
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Off"),
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Information"),
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Warning"),
QT_TRANSLATE_NOOP("SpeedLimitSettings", "Assist"),
};
@@ -23,11 +23,11 @@ SpeedLimitPolicy::SpeedLimitPolicy(QWidget *parent) : QWidget(parent) {
ListWidgetSP *list = new ListWidgetSP(this);
std::vector<QString> speed_limit_policy_texts{
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_ONLY)],
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_ONLY)],
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_FIRST)],
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_FIRST)],
SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::COMBINED)]
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_ONLY)].toStdString().c_str()),
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_ONLY)].toStdString().c_str()),
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::CAR_FIRST)].toStdString().c_str()),
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::MAP_FIRST)].toStdString().c_str()),
tr(SpeedLimitSourcePolicyTexts[static_cast<int>(SpeedLimitSourcePolicy::COMBINED)].toStdString().c_str())
};
speed_limit_policy = new ButtonParamControlSP(
"SpeedLimitPolicy",
@@ -7,6 +7,8 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/speed_limit/speed_limit_settings.h"
#include "selfdrive/ui/sunnypilot/qt/util.h"
SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent) {
subPanelFrame = new QFrame();
QVBoxLayout *subPanelLayout = new QVBoxLayout(subPanelFrame);
@@ -25,10 +27,10 @@ SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent)
speedLimitPolicyScreen = new SpeedLimitPolicy(this);
std::vector<QString> speed_limit_mode_texts{
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::OFF)],
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::INFORMATION)],
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::WARNING)],
SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::ASSIST)],
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::OFF)].toStdString().c_str()),
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::INFORMATION)].toStdString().c_str()),
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::WARNING)].toStdString().c_str()),
tr(SpeedLimitModeTexts[static_cast<int>(SpeedLimitMode::ASSIST)].toStdString().c_str())
};
speed_limit_mode_settings = new ButtonParamControlSP(
"SpeedLimitMode",
@@ -64,9 +66,9 @@ SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent)
QVBoxLayout *offsetLayout = new QVBoxLayout(offsetFrame);
std::vector<QString> speed_limit_offset_texts{
SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::NONE)],
SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::FIXED)],
SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::PERCENT)]
tr(SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::NONE)].toStdString().c_str()),
tr(SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::FIXED)].toStdString().c_str()),
tr(SpeedLimitOffsetTypeTexts[static_cast<int>(SpeedLimitOffsetType::PERCENT)].toStdString().c_str())
};
speed_limit_offset_settings = new ButtonParamControlSP(
"SpeedLimitOffsetType",
@@ -103,13 +105,13 @@ SpeedLimitSettings::SpeedLimitSettings(QWidget *parent) : QStackedWidget(parent)
}
void SpeedLimitSettings::refresh() {
bool is_release = params.getBool("IsReleaseSpBranch");
bool is_metric_param = params.getBool("IsMetric");
SpeedLimitMode speed_limit_mode_param = static_cast<SpeedLimitMode>(std::atoi(params.get("SpeedLimitMode").c_str()));
SpeedLimitOffsetType offset_type_param = static_cast<SpeedLimitOffsetType>(std::atoi(params.get("SpeedLimitOffsetType").c_str()));
QString offsetLabel = QString::fromStdString(params.get("SpeedLimitValueOffset"));
bool has_longitudinal_control;
bool intelligent_cruise_button_management_available;
bool sla_available;
auto cp_bytes = params.get("CarParamsPersistent");
auto cp_sp_bytes = params.get("CarParamsSPPersistent");
if (!cp_bytes.empty() && !cp_sp_bytes.empty()) {
@@ -120,11 +122,24 @@ void SpeedLimitSettings::refresh() {
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
cereal::CarParamsSP::Reader CP_SP = cmsg_sp.getRoot<cereal::CarParamsSP>();
has_longitudinal_control = hasLongitudinalControl(CP);
intelligent_cruise_button_management_available = CP_SP.getIntelligentCruiseButtonManagementAvailable();
bool has_longitudinal_control = hasLongitudinalControl(CP);
bool has_icbm = hasIntelligentCruiseButtonManagement(CP_SP);
/*
* Speed Limit Assist is available when:
* - has_longitudinal_control or has_icbm, and
* - is not a release branch or not a disallowed brand, and
* - is not always disallowed
*/
bool sla_disallow_in_release = CP.getBrand() == "tesla" && is_release;
bool sla_always_disallow = CP.getBrand() == "rivian";
sla_available = (has_longitudinal_control || has_icbm) && !sla_disallow_in_release && !sla_always_disallow;
if (!sla_available && speed_limit_mode_param == SpeedLimitMode::ASSIST) {
params.put("SpeedLimitMode", std::to_string(static_cast<int>(SpeedLimitMode::WARNING)));
}
} else {
has_longitudinal_control = false;
intelligent_cruise_button_management_available = false;
sla_available = false;
}
speed_limit_mode_settings->setDescription(modeDescription(speed_limit_mode_param));
@@ -144,13 +159,14 @@ void SpeedLimitSettings::refresh() {
speed_limit_offset->showDescription();
}
if (has_longitudinal_control || intelligent_cruise_button_management_available) {
if (sla_available) {
speed_limit_mode_settings->setEnableSelectedButtons(true, convertSpeedLimitModeValues(getSpeedLimitModeValues()));
} else {
speed_limit_mode_settings->setEnableSelectedButtons(true, convertSpeedLimitModeValues(
{SpeedLimitMode::OFF,SpeedLimitMode::INFORMATION, SpeedLimitMode::WARNING}));
{SpeedLimitMode::OFF, SpeedLimitMode::INFORMATION, SpeedLimitMode::WARNING}));
}
speed_limit_mode_settings->refresh();
speed_limit_mode_settings->showDescription();
speed_limit_offset->showDescription();
}
@@ -35,6 +35,7 @@ private:
SpeedLimitPolicy *speedLimitPolicyScreen;
ButtonParamControlSP *speed_limit_offset_settings;
OptionControlSP *speed_limit_offset;
bool icbm_available = false;
static QString offsetDescription(SpeedLimitOffsetType type = SpeedLimitOffsetType::NONE) {
QString none_str = tr("⦿ None: No Offset");
@@ -7,6 +7,8 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h"
#include "selfdrive/ui/sunnypilot/qt/util.h"
LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
setStyleSheet(R"(
#back_btn {
@@ -36,13 +38,24 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
intelligentCruiseButtonManagement = new ParamControlSP(
"IntelligentCruiseButtonManagement",
tr("Intelligent Cruise Button Management (ICBM) (Alpha)"),
tr("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control."),
"",
"",
this
);
intelligentCruiseButtonManagement->setConfirmation(true, false);
QObject::connect(intelligentCruiseButtonManagement, &ParamControlSP::toggleFlipped, this, [=](bool) {
refresh(offroad);
});
list->addItem(intelligentCruiseButtonManagement);
dynamicExperimentalControl = new ParamControlSP(
"DynamicExperimentalControl",
tr("Dynamic Experimental Control (DEC)"),
tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."),
"",
this
);
list->addItem(dynamicExperimentalControl);
SmartCruiseControlVision = new ParamControl(
"SmartCruiseControlVision",
tr("Smart Cruise Control - Vision"),
@@ -75,35 +88,6 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
main_layout->setCurrentWidget(cruisePanelScreen);
});
// Vibe Personality Controller
vibePersonalityControl = new ParamControlSP("VibePersonalityEnabled",
tr("Vibe Personality Controller"),
tr("Advanced driving personality system with separate controls for acceleration behavior (Eco/Normal/Sport) and following distance/braking (Relaxed/Standard/Aggressive). "
"Customize your driving experience with independent acceleration and distance personalities."),
"../assets/offroad/icon_shell.png");
list->addItem(vibePersonalityControl);
connect(vibePersonalityControl, &ParamControlSP::toggleFlipped, [=]() {
refresh(offroad);
});
// Vibe Acceleration Personality
vibeAccelPersonalityControl = new ParamControlSP("VibeAccelPersonalityEnabled",
tr("Acceleration Personality"),
tr("Controls acceleration behavior: Eco (efficient), Normal (balanced), Sport (responsive). "
"Adjust how aggressively the vehicle accelerates while maintaining smooth operation."),
"../assets/offroad/icon_shell.png");
list->addItem(vibeAccelPersonalityControl);
// Vibe Following Distance Personality
vibeFollowPersonalityControl = new ParamControlSP("VibeFollowPersonalityEnabled",
tr("Following Distance Personality"),
tr("Controls following distance and braking behavior: Relaxed (longer distance, gentler braking), Standard (balanced), Aggressive (shorter distance, firmer braking). "
"Fine-tune your comfort level in traffic situations."),
"../assets/offroad/icon_shell.png");
list->addItem(vibeFollowPersonalityControl);
main_layout->addWidget(cruisePanelScreen);
main_layout->addWidget(speedLimitScreen);
main_layout->setCurrentWidget(cruisePanelScreen);
@@ -120,6 +104,8 @@ void LongitudinalPanel::hideEvent(QHideEvent *event) {
}
void LongitudinalPanel::refresh(bool _offroad) {
const QString icbm_description = tr("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control.");
auto cp_bytes = params.get("CarParamsPersistent");
auto cp_sp_bytes = params.get("CarParamsSPPersistent");
if (!cp_bytes.empty() && !cp_sp_bytes.empty()) {
@@ -132,15 +118,61 @@ void LongitudinalPanel::refresh(bool _offroad) {
has_longitudinal_control = hasLongitudinalControl(CP);
is_pcm_cruise = CP.getPcmCruise();
intelligent_cruise_button_management_available = CP_SP.getIntelligentCruiseButtonManagementAvailable();
has_icbm = hasIntelligentCruiseButtonManagement(CP_SP);
if (CP_SP.getIntelligentCruiseButtonManagementAvailable() && !has_longitudinal_control) {
intelligentCruiseButtonManagement->setEnabled(offroad);
intelligentCruiseButtonManagement->setDescription(icbm_description);
} else {
params.remove("IntelligentCruiseButtonManagement");
intelligentCruiseButtonManagement->setEnabled(false);
const QString icbm_unavaialble = tr("Intelligent Cruise Button Management is currently unavailable on this platform.");
QString long_desc = icbm_unavaialble;
if (has_longitudinal_control) {
if (CP.getAlphaLongitudinalAvailable()) {
long_desc = icbm_unavaialble + " " + tr("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.");
} else {
long_desc = icbm_unavaialble + " " + tr("sunnypilot Longitudinal Control is the default longitudinal control for this platform.");
}
}
intelligentCruiseButtonManagement->setDescription("<b>" + long_desc + "</b><br><br>" + icbm_description);
intelligentCruiseButtonManagement->showDescription();
}
if (has_longitudinal_control || has_icbm) {
// enable Custom ACC Increments when long is available and is not PCM cruise
customAccIncrement->setEnabled(((has_longitudinal_control && !is_pcm_cruise) || has_icbm) && offroad);
dynamicExperimentalControl->setEnabled(has_longitudinal_control);
SmartCruiseControlVision->setEnabled(true);
SmartCruiseControlMap->setEnabled(true);
} else {
params.remove("CustomAccIncrementsEnabled");
params.remove("DynamicExperimentalControl");
params.remove("SmartCruiseControlVision");
params.remove("SmartCruiseControlMap");
customAccIncrement->setEnabled(false);
dynamicExperimentalControl->setEnabled(false);
SmartCruiseControlVision->setEnabled(false);
SmartCruiseControlMap->setEnabled(false);
}
intelligentCruiseButtonManagement->refresh();
customAccIncrement->refresh();
dynamicExperimentalControl->refresh();
SmartCruiseControlVision->refresh();
SmartCruiseControlMap->refresh();
} else {
has_longitudinal_control = false;
is_pcm_cruise = false;
intelligent_cruise_button_management_available = false;
has_icbm = false;
intelligentCruiseButtonManagement->setDescription("<b>" + tr("Start the vehicle to check vehicle compatibility.") + "</br><b><b>" + icbm_description);
}
QString accEnabledDescription = tr("Enable custom Short & Long press increments for cruise speed increase/decrease.");
QString accNoLongDescription = tr("This feature can only be used with openpilot longitudinal control enabled.");
QString accNoLongDescription = tr("This feature can only be used with sunnypilot longitudinal control enabled.");
QString accPcmCruiseDisabledDescription = tr("This feature is not supported on this platform due to vehicle limitations.");
QString onroadOnlyDescription = tr("Start the vehicle to check vehicle compatibility.");
@@ -148,48 +180,19 @@ void LongitudinalPanel::refresh(bool _offroad) {
customAccIncrement->setDescription(onroadOnlyDescription);
customAccIncrement->showDescription();
} else {
if (has_longitudinal_control || intelligent_cruise_button_management_available) {
if (is_pcm_cruise) {
if (has_longitudinal_control || has_icbm) {
if (has_longitudinal_control && is_pcm_cruise) {
customAccIncrement->setDescription(accPcmCruiseDisabledDescription);
customAccIncrement->showDescription();
} else {
customAccIncrement->setDescription(accEnabledDescription);
}
} else {
params.remove("CustomAccIncrementsEnabled");
customAccIncrement->toggleFlipped(false);
customAccIncrement->setDescription(accNoLongDescription);
customAccIncrement->showDescription();
params.remove("IntelligentCruiseButtonManagement");
intelligentCruiseButtonManagement->toggleFlipped(false);
}
}
bool vibePersonalityEnabled = params.getBool("VibePersonalityEnabled");
if (vibePersonalityEnabled) {
vibeAccelPersonalityControl->setVisible(true);
vibeFollowPersonalityControl->setVisible(true);
} else {
vibeAccelPersonalityControl->setVisible(false);
vibeFollowPersonalityControl->setVisible(false);
}
bool icbm_allowed = intelligent_cruise_button_management_available && !has_longitudinal_control;
intelligentCruiseButtonManagement->setEnabled(icbm_allowed && offroad);
// enable toggle when long is available and is not PCM cruise
bool cai_allowed = (has_longitudinal_control && !is_pcm_cruise) || icbm_allowed;
customAccIncrement->setEnabled(cai_allowed && !offroad);
customAccIncrement->refresh();
SmartCruiseControlVision->setEnabled(has_longitudinal_control || icbm_allowed);
SmartCruiseControlMap->setEnabled(has_longitudinal_control || icbm_allowed);
// Vibe Personality controls - always enabled for toggling
vibePersonalityControl->setEnabled(true);
vibeAccelPersonalityControl->setEnabled(true);
vibeFollowPersonalityControl->setEnabled(true);
vibePersonalityControl->refresh();
vibeAccelPersonalityControl->refresh();
vibeFollowPersonalityControl->refresh();
offroad = _offroad;
}
@@ -25,7 +25,7 @@ private:
Params params;
bool has_longitudinal_control = false;
bool is_pcm_cruise = false;
bool intelligent_cruise_button_management_available = false;;
bool has_icbm = false;
bool offroad = false;
QStackedLayout *main_layout = nullptr;
@@ -35,10 +35,7 @@ private:
ParamControl *SmartCruiseControlVision;
ParamControl *SmartCruiseControlMap;
ParamControl *intelligentCruiseButtonManagement = nullptr;
ParamControl *dynamicExperimentalControl = nullptr;
SpeedLimitSettings *speedLimitScreen;
PushButtonSP *speedLimitSettings;
ParamControlSP *vibePersonalityControl;
ParamControlSP *vibeAccelPersonalityControl;
ParamControlSP *vibeFollowPersonalityControl;
};
@@ -33,7 +33,7 @@ private:
static QString toggleDisableMsg(bool _offroad, bool _has_longitudinal_control) {
if (!_has_longitudinal_control) {
return tr("This feature can only be used with openpilot longitudinal control enabled.");
return tr("This feature can only be used with sunnypilot longitudinal control enabled.");
}
if (!_offroad) {
@@ -57,7 +57,7 @@ private:
}
return QString("%1<br><br>%2<br>%3<br>%4<br>")
.arg(tr("Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control."))
.arg(tr("Fine-tune your driving experience by adjusting acceleration smoothness with sunnypilot longitudinal control."))
.arg(off_str)
.arg(dynamic_str)
.arg(predictive_str);
@@ -8,7 +8,52 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/subaru_settings.h"
SubaruSettings::SubaruSettings(QWidget *parent) : BrandSettingsInterface(parent) {
stopAndGoToggle = new ParamControl("SubaruStopAndGo", tr("Stop and Go (Beta)"), "", "");
stopAndGoToggle->setConfirmation(true, false);
list->addItem(stopAndGoToggle);
stopAndGoManualParkingBrakeToggle = new ParamControl(
"SubaruStopAndGoManualParkingBrake",
tr("Stop and Go for Manual Parking Brake (Beta)"),
"",
""
);
stopAndGoManualParkingBrakeToggle->setConfirmation(true, false);
list->addItem(stopAndGoManualParkingBrakeToggle);
}
void SubaruSettings::updateSettings() {
auto cp_bytes = params.get("CarParamsPersistent");
if (!cp_bytes.empty()) {
AlignedBuffer aligned_buf;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
is_subaru = CP.getBrand() == "subaru";
if (is_subaru) {
if (!(CP.getFlags() & (SUBARU_FLAG_GLOBAL_GEN2 | SUBARU_FLAG_HYBRID))) {
has_stop_and_go = true;
}
}
} else {
is_subaru = false;
has_stop_and_go = false;
}
bool stop_and_go_disabled = !offroad || !has_stop_and_go;
QString stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc);
QString stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc);
if (stop_and_go_disabled) {
stop_and_go_desc = stopAndGoDescriptionBuilder(stopAndGoDesc, stopAndGoDisabledMsg());
stop_and_go_manual_parking_brake_desc = stopAndGoDescriptionBuilder(stopAndGoManualParkingBrakeDesc, stopAndGoDisabledMsg());
}
stopAndGoToggle->setEnabled(has_stop_and_go);
stopAndGoToggle->setDescription(stop_and_go_desc);
stopAndGoToggle->showDescription();
stopAndGoManualParkingBrakeToggle->setEnabled(has_stop_and_go);
stopAndGoManualParkingBrakeToggle->setDescription(stop_and_go_manual_parking_brake_desc);
stopAndGoManualParkingBrakeToggle->showDescription();
}
@@ -14,6 +14,9 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
const int SUBARU_FLAG_GLOBAL_GEN2 = 4;
const int SUBARU_FLAG_HYBRID = 32;
class SubaruSettings : public BrandSettingsInterface {
Q_OBJECT
@@ -23,4 +26,32 @@ public:
private:
bool offroad = false;
bool is_subaru;
bool has_stop_and_go;
ParamControl* stopAndGoToggle;
ParamControl* stopAndGoManualParkingBrakeToggle;
QString stopAndGoDesc = tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms.");
QString stopAndGoManualParkingBrakeDesc = tr("Experimental feature to enable stop and go for Subaru Global models with manual handbrake. Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!");
QString stopAndGoDisabledMsg() const {
if (is_subaru && !has_stop_and_go) {
return tr("This feature is currently not available on this platform.");
}
if (!is_subaru) {
return tr("Start the car to check car compatibility.");
}
if (!offroad) {
return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.");
}
return QString();
}
static QString stopAndGoDescriptionBuilder(const QString &base_description, const QString &custom_description = "") {
return "<b>" + custom_description + "</b><br><br>" + base_description;
}
};
@@ -119,7 +119,7 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) {
// Visuals: Display Metrics below Chevron
std::vector<QString> chevron_info_settings_texts{tr("Off"), tr("Distance"), tr("Speed"), tr("Time"), tr("All")};
chevron_info_settings = new ButtonParamControlSP(
"ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control)."),
"ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with sunnypilot longitudinal control)."),
"",
chevron_info_settings_texts,
200);
@@ -159,8 +159,8 @@ void VisualsPanel::refreshLongitudinalStatus() {
}
if (chevron_info_settings) {
QString chevronEnabledDescription = tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control).");
QString chevronNoLongDescription = tr("This feature requires openpilot longitudinal control to be available.");
QString chevronEnabledDescription = tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with sunnypilot longitudinal control).");
QString chevronNoLongDescription = tr("This feature requires sunnypilot longitudinal control to be available.");
if (has_longitudinal_control) {
chevron_info_settings->setDescription(chevronEnabledDescription);
+90 -65
View File
@@ -12,43 +12,65 @@
HudRendererSP::HudRendererSP() {
plus_arrow_up_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_plus_arrow_up", {105, 105});
minus_arrow_down_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_minus_arrow_down", {105, 105});
plus_arrow_up_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_plus_arrow_up", {90, 90});
minus_arrow_down_img = loadPixmap("../../sunnypilot/selfdrive/assets/img_minus_arrow_down", {90, 90});
int small_max = e2e_alert_small * 2 - 40;
int large_max = e2e_alert_large * 2 - 40;
green_light_alert_small_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/green_light.png", {small_max, small_max});
green_light_alert_large_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/green_light.png", {large_max, large_max});
lead_depart_alert_small_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/lead_depart.png", {small_max, small_max});
lead_depart_alert_large_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/lead_depart.png", {large_max, large_max});
int size = e2e_alert_size * 2 - 40;
green_light_alert_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/green_light.png", {size, size});
lead_depart_alert_img = loadPixmap("../../sunnypilot/selfdrive/assets/images/lead_depart.png", {size, size});
}
void HudRendererSP::updateState(const UIState &s) {
HudRenderer::updateState(s);
float speedConv = is_metric ? MS_TO_KPH : MS_TO_MPH;
devUiInfo = s.scene.dev_ui_info;
roadName = s.scene.road_name;
showTurnSignals = s.scene.turn_signals;
speedLimitMode = static_cast<SpeedLimitMode>(s.scene.speed_limit_mode);
speedUnit = is_metric ? tr("km/h") : tr("mph");
standstillTimer = s.scene.standstill_timer;
const SubMaster &sm = *(s.sm);
const auto cs = sm["controlsState"].getControlsState();
const auto car_state = sm["carState"].getCarState();
const auto car_control = sm["carControl"].getCarControl();
const auto radar_state = sm["radarState"].getRadarState();
const auto is_gps_location_external = sm.rcv_frame("gpsLocationExternal") > 1;
const auto gpsLocation = is_gps_location_external ? sm["gpsLocationExternal"].getGpsLocationExternal() : sm["gpsLocation"].getGpsLocation();
const char *gps_source = is_gps_location_external ? "gpsLocationExternal" : "gpsLocation";
const auto gpsLocation = is_gps_location_external ? sm[gps_source].getGpsLocationExternal() : sm[gps_source].getGpsLocation();
const auto ltp = sm["liveTorqueParameters"].getLiveTorqueParameters();
const auto car_params = sm["carParams"].getCarParams();
const auto car_params_sp = sm["carParamsSP"].getCarParamsSP();
const auto lp_sp = sm["longitudinalPlanSP"].getLongitudinalPlanSP();
const auto lmd = sm["liveMapDataSP"].getLiveMapDataSP();
float speedConv = is_metric ? MS_TO_KPH : MS_TO_MPH;
speedLimit = lp_sp.getSpeedLimit().getResolver().getSpeedLimit() * speedConv;
speedLimitLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLast() * speedConv;
speedLimitOffset = lp_sp.getSpeedLimit().getResolver().getSpeedLimitOffset() * speedConv;
speedLimitValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitValid();
speedLimitLastValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLastValid();
speedLimitFinalLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitFinalLast() * speedConv;
speedLimitMode = static_cast<SpeedLimitMode>(s.scene.speed_limit_mode);
speedLimitAssistState = lp_sp.getSpeedLimit().getAssist().getState();
speedLimitAssistActive = lp_sp.getSpeedLimit().getAssist().getActive();
roadName = s.scene.road_name;
if (sm.updated("carParams")) {
steerControlType = car_params.getSteerControlType();
}
if (sm.updated("carParamsSP")) {
pcmCruiseSpeed = car_params_sp.getPcmCruiseSpeed();
}
if (sm.updated("longitudinalPlanSP")) {
speedLimit = lp_sp.getSpeedLimit().getResolver().getSpeedLimit() * speedConv;
speedLimitLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLast() * speedConv;
speedLimitOffset = lp_sp.getSpeedLimit().getResolver().getSpeedLimitOffset() * speedConv;
speedLimitValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitValid();
speedLimitLastValid = lp_sp.getSpeedLimit().getResolver().getSpeedLimitLastValid();
speedLimitFinalLast = lp_sp.getSpeedLimit().getResolver().getSpeedLimitFinalLast() * speedConv;
speedLimitSource = lp_sp.getSpeedLimit().getResolver().getSource();
speedLimitAssistState = lp_sp.getSpeedLimit().getAssist().getState();
speedLimitAssistActive = lp_sp.getSpeedLimit().getAssist().getActive();
smartCruiseControlVisionEnabled = lp_sp.getSmartCruiseControl().getVision().getEnabled();
smartCruiseControlVisionActive = lp_sp.getSmartCruiseControl().getVision().getActive();
smartCruiseControlMapEnabled = lp_sp.getSmartCruiseControl().getMap().getEnabled();
smartCruiseControlMapActive = lp_sp.getSmartCruiseControl().getMap().getActive();
}
greenLightAlert = lp_sp.getE2eAlerts().getGreenLightAlert();
leadDepartAlert = lp_sp.getE2eAlerts().getLeadDepartAlert();
if (sm.updated("liveMapDataSP")) {
roadNameStr = QString::fromStdString(lmd.getRoadName());
speedLimitAheadValid = lmd.getSpeedLimitAheadValid();
@@ -64,7 +86,7 @@ void HudRendererSP::updateState(const UIState &s) {
static int reverse_delay = 0;
bool reverse_allowed = false;
if (int(car_state.getGearShifter()) != 4) {
if (car_state.getGearShifter() != cereal::CarState::GearShifter::REVERSE) {
reverse_delay = 0;
reverse_allowed = false;
} else {
@@ -76,46 +98,47 @@ void HudRendererSP::updateState(const UIState &s) {
reversing = reverse_allowed;
if (sm.updated("liveParameters")) {
roll = sm["liveParameters"].getLiveParameters().getRoll();
}
if (sm.updated("deviceState")) {
memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent();
}
if (sm.updated(gps_source)) {
gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not.
altitude = gpsLocation.getAltitude();
bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg();
bearingDeg = gpsLocation.getBearingDeg();
}
if (sm.updated("liveTorqueParameters")) {
torquedUseParams = ltp.getUseParams();
latAccelFactorFiltered = ltp.getLatAccelFactorFiltered();
frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered();
liveValid = ltp.getLiveValid();
}
latActive = car_control.getLatActive();
actuators = car_control.getActuators();
longOverride = car_control.getCruiseControl().getOverride();
carControlEnabled = car_control.getEnabled();
steerOverride = car_state.getSteeringPressed();
devUiInfo = s.scene.dev_ui_info;
speedUnit = is_metric ? tr("km/h") : tr("mph");
lead_d_rel = radar_state.getLeadOne().getDRel();
lead_v_rel = radar_state.getLeadOne().getVRel();
lead_status = radar_state.getLeadOne().getStatus();
steerControlType = car_params.getSteerControlType();
actuators = car_control.getActuators();
torqueLateral = steerControlType == cereal::CarParams::SteerControlType::TORQUE;
angleSteers = car_state.getSteeringAngleDeg();
desiredCurvature = cs.getDesiredCurvature();
curvature = cs.getCurvature();
roll = sm["liveParameters"].getLiveParameters().getRoll();
memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent();
gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not.
altitude = gpsLocation.getAltitude();
vEgo = car_state.getVEgo();
aEgo = car_state.getAEgo();
steeringTorqueEps = car_state.getSteeringTorqueEps();
bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg();
bearingDeg = gpsLocation.getBearingDeg();
torquedUseParams = ltp.getUseParams();
latAccelFactorFiltered = ltp.getLatAccelFactorFiltered();
frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered();
liveValid = ltp.getLiveValid();
standstillTimer = s.scene.standstill_timer;
isStandstill = car_state.getStandstill();
if (not s.scene.started) standstillElapsedTime = 0.0;
longOverride = car_control.getCruiseControl().getOverride();
smartCruiseControlVisionEnabled = lp_sp.getSmartCruiseControl().getVision().getEnabled();
smartCruiseControlVisionActive = lp_sp.getSmartCruiseControl().getVision().getActive();
smartCruiseControlMapEnabled = lp_sp.getSmartCruiseControl().getMap().getEnabled();
smartCruiseControlMapActive = lp_sp.getSmartCruiseControl().getMap().getActive();
greenLightAlert = lp_sp.getE2eAlerts().getGreenLightAlert();
leadDepartAlert = lp_sp.getE2eAlerts().getLeadDepartAlert();
if (!s.scene.started) standstillElapsedTime = 0.0;
// override stock current speed values
float v_ego = (v_ego_cluster_seen && !s.scene.trueVEgoUI) ? car_state.getVEgoCluster() : car_state.getVEgo();
@@ -126,10 +149,11 @@ void HudRendererSP::updateState(const UIState &s) {
rightBlinkerOn = car_state.getRightBlinker();
leftBlindspot = car_state.getLeftBlindspot();
rightBlindspot = car_state.getRightBlindspot();
showTurnSignals = s.scene.turn_signals;
carControlEnabled = car_control.getEnabled();
speedCluster = car_state.getCruiseState().getSpeedCluster() * speedConv;
allow_e2e_alerts = sm["selfdriveState"].getSelfdriveState().getAlertSize() == cereal::SelfdriveState::AlertSize::NONE &&
sm.rcv_frame("driverStateV2") > s.scene.started_frame && !reversing;
}
void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
@@ -222,7 +246,7 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
drawRoadName(p, surface_rect);
// Green Light & Lead Depart Alerts
if (greenLightAlert or leadDepartAlert) {
if (greenLightAlert || leadDepartAlert) {
e2eAlertDisplayTimer = 3 * UI_FREQ;
// reset onroad sleep timer for e2e alerts
uiStateSP()->reset_onroad_sleep_timer();
@@ -232,11 +256,11 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
e2eAlertFrame++;
if (greenLightAlert) {
alert_text = tr("GREEN\nLIGHT");
alert_img = devUiInfo > 0 ? green_light_alert_small_img : green_light_alert_large_img;
alert_img = green_light_alert_img;
}
else if (leadDepartAlert) {
alert_text = tr("LEAD VEHICLE\nDEPARTING");
alert_img = devUiInfo > 0 ? lead_depart_alert_small_img : lead_depart_alert_large_img;
alert_img = lead_depart_alert_img;
}
drawE2eAlert(p, surface_rect);
}
@@ -254,7 +278,7 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) {
// No Alerts displayed
else {
e2eAlertFrame = 0;
if (not isStandstill) standstillElapsedTime = 0.0;
if (!isStandstill) standstillElapsedTime = 0.0;
}
// Blinker
@@ -545,7 +569,8 @@ void HudRendererSP::drawSpeedLimitSigns(QPainter &p, QRect &sign_rect) {
}
void HudRendererSP::drawUpcomingSpeedLimit(QPainter &p) {
bool speed_limit_ahead = speedLimitAheadValid && speedLimitAhead > 0 && speedLimitAhead != speedLimit && speedLimitAheadValidFrame > 0;
bool speed_limit_ahead = speedLimitAheadValid && speedLimitAhead > 0 && speedLimitAhead != speedLimit && speedLimitAheadValidFrame > 0 &&
speedLimitSource == cereal::LongitudinalPlanSP::SpeedLimit::Source::MAP;
if (!speed_limit_ahead) {
return;
}
@@ -639,7 +664,7 @@ void HudRendererSP::drawRoadName(QPainter &p, const QRect &surface_rect) {
void HudRendererSP::drawSpeedLimitPreActiveArrow(QPainter &p, QRect &sign_rect) {
const int sign_margin = 12;
const int arrow_spacing = sign_margin * 3;
const int arrow_spacing = sign_margin * 1.4;
int arrow_x = sign_rect.right() + arrow_spacing;
int _set_speed = std::nearbyint(set_speed);
@@ -689,7 +714,7 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
}
// Draw "MAX" or carState.cruiseState.speedCluster (when ICBM is active) text
if (carControlEnabled) {
if (!pcmCruiseSpeed && carControlEnabled) {
if (std::nearbyint(set_speed) != std::nearbyint(speedCluster)) {
icbm_active_counter = 3 * UI_FREQ;
} else if (icbm_active_counter > 0) {
@@ -714,26 +739,26 @@ void HudRendererSP::drawSetSpeedSP(QPainter &p, const QRect &surface_rect) {
}
void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const QString &alert_alt_text) {
int size = devUiInfo > 0 ? e2e_alert_small : e2e_alert_large;
int x = surface_rect.center().x() + surface_rect.width() / 4;
if (!allow_e2e_alerts) return;
int x = surface_rect.right() - e2e_alert_size - (devUiInfo > 0 ? 180 : 100) - (UI_BORDER_SIZE * 3);
int y = surface_rect.center().y() + 20;
x += devUiInfo > 0 ? 0 : 50;
QRect alertRect(x - size, y - size, size * 2, size * 2);
QRect alertRect(x - e2e_alert_size, y - e2e_alert_size, e2e_alert_size * 2, e2e_alert_size * 2);
// Alert Circle
QPoint center = alertRect.center();
QColor frameColor;
if (not alert_alt_text.isEmpty()) frameColor = QColor(255, 255, 255, 75);
if (!alert_alt_text.isEmpty()) frameColor = QColor(255, 255, 255, 75);
else frameColor = pulseElement(e2eAlertFrame) ? QColor(255, 255, 255, 75) : QColor(0, 255, 0, 75);
p.setPen(QPen(frameColor, 15));
p.setBrush(QColor(0, 0, 0, 190));
p.drawEllipse(center, size, size);
p.drawEllipse(center, e2e_alert_size, e2e_alert_size);
// Alert Text
QColor txtColor;
QFont font;
int alert_bottom_adjustment;
if (not alert_alt_text.isEmpty()) {
if (!alert_alt_text.isEmpty()) {
font = InterFont(100, QFont::Bold);
alert_bottom_adjustment = 5;
txtColor = QColor(255, 255, 255, 255);
@@ -750,7 +775,7 @@ void HudRendererSP::drawE2eAlert(QPainter &p, const QRect &surface_rect, const Q
textRect.moveBottom(alertRect.bottom() - alertRect.height() / alert_bottom_adjustment);
p.drawText(textRect, Qt::AlignCenter, alert_text);
if (not alert_alt_text.isEmpty()) {
if (!alert_alt_text.isEmpty()) {
// Alert Alternate Text
p.setFont(InterFont(80, QFont::Bold));
p.setPen(QColor(255, 175, 3, 240));
@@ -779,7 +804,7 @@ void HudRendererSP::drawCurrentSpeedSP(QPainter &p, const QRect &surface_rect) {
void HudRendererSP::drawBlinker(QPainter &p, const QRect &surface_rect) {
const bool hazard = leftBlinkerOn && rightBlinkerOn;
int blinkerStatus = hazard ? 2 : (leftBlinkerOn or rightBlinkerOn) ? 1 : 0;
int blinkerStatus = hazard ? 2 : (leftBlinkerOn || rightBlinkerOn) ? 1 : 0;
if (!leftBlinkerOn && !rightBlinkerOn) {
blinkerFrameCounter = 0;
+6 -6
View File
@@ -83,6 +83,7 @@ private:
bool speedLimitValid;
bool speedLimitLastValid;
float speedLimitFinalLast;
cereal::LongitudinalPlanSP::SpeedLimit::Source speedLimitSource;
bool speedLimitAheadValid;
float speedLimitAhead;
float speedLimitAheadDistance;
@@ -96,16 +97,14 @@ private:
int speedLimitAssistFrame;
QPixmap plus_arrow_up_img;
QPixmap minus_arrow_down_img;
int e2e_alert_small = 250;
int e2e_alert_large = 300;
QPixmap green_light_alert_small_img;
QPixmap green_light_alert_large_img;
int e2e_alert_size = 250;
QPixmap green_light_alert_img;
bool greenLightAlert;
int e2eAlertFrame;
int e2eAlertDisplayTimer = 0;
bool allow_e2e_alerts;
bool leadDepartAlert;
QPixmap lead_depart_alert_small_img;
QPixmap lead_depart_alert_large_img;
QPixmap lead_depart_alert_img;
QString alert_text;
QPixmap alert_img;
bool hideVEgoUI;
@@ -120,4 +119,5 @@ private:
bool carControlEnabled;
float speedCluster = 0;
int icbm_active_counter = 0;
bool pcmCruiseSpeed = true;
};
+90 -61
View File
@@ -21,74 +21,73 @@ void ModelRendererSP::update_model(const cereal::ModelDataV2::Reader &model, con
mapLineToPolygon(model.getLaneLines()[2], 0.2, -0.05, &right_blindspot_vertices, max_idx_barrier);
}
void ModelRendererSP::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) {
void ModelRendererSP::draw(QPainter &painter, const QRect &surface_rect) {
auto *s = uiState();
auto &sm = *(s->sm);
bool blindspot = s->scene.blindspot_ui;
if (blindspot) {
bool left_blindspot = sm["carState"].getCarState().getLeftBlindspot();
bool right_blindspot = sm["carState"].getCarState().getRightBlindspot();
//painter.setBrush(QColor::fromRgbF(1.0, 0.0, 0.0, 0.4)); // Red with alpha for blind spot
if (left_blindspot && !left_blindspot_vertices.isEmpty()) {
QLinearGradient gradient(0, 0, surface_rect.width(), 0); // Horizontal gradient from left to right
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
painter.setBrush(gradient);
painter.drawPolygon(left_blindspot_vertices);
}
if (right_blindspot && !right_blindspot_vertices.isEmpty()) {
QLinearGradient gradient(surface_rect.width(), 0, 0, 0); // Horizontal gradient from right to left
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
painter.setBrush(gradient);
painter.drawPolygon(right_blindspot_vertices);
}
if (sm.rcv_frame("liveCalibration") < s->scene.started_frame ||
sm.rcv_frame("modelV2") < s->scene.started_frame) {
return;
}
bool rainbow = s->scene.rainbow_mode;
//float v_ego = sm["carState"].getCarState().getVEgo();
clip_region = surface_rect.adjusted(-CLIP_MARGIN, -CLIP_MARGIN, CLIP_MARGIN, CLIP_MARGIN);
experimental_mode = sm["selfdriveState"].getSelfdriveState().getExperimentalMode();
longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl();
path_offset_z = sm["liveCalibration"].getLiveCalibration().getHeight()[0];
if (rainbow) {
// Simple time-based animation
float time_offset = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count() / 1000.0f;
painter.save();
// simple linear gradient from bottom to top
QLinearGradient bg(0, surface_rect.height(), 0, 0);
const auto &model = sm["modelV2"].getModelV2();
const auto &radar_state = sm["radarState"].getRadarState();
const auto &lead_one = radar_state.getLeadOne();
const auto &car_state = sm["carState"].getCarState();
// evenly spaced colors across the spectrum
// The animation shifts the entire spectrum smoothly
float animation_speed = 40.0f; // speed vroom vroom
float hue_offset = fmod(time_offset * animation_speed, 360.0f);
update_model(model, lead_one);
drawLaneLines(painter);
// 6-8 color stops for smooth transitions more color makes it laggy
const int num_stops = 7;
for (int i = 0; i < num_stops; i++) {
float position = static_cast<float>(i) / (num_stops - 1);
float hue = fmod(hue_offset + position * 360.0f, 360.0f);
float saturation = 0.9f;
float lightness = 0.6f;
// Alpha fades out towards the far end of the path
float alpha = 0.8f * (1.0f - position * 0.3f);
QColor color = QColor::fromHslF(hue / 360.0f, saturation, lightness, alpha);
bg.setColorAt(position, color);
}
painter.setBrush(bg);
painter.drawPolygon(track_vertices);
if (s->scene.rainbow_mode) {
drawRainbowPath(painter, surface_rect);
} else {
// Normal path rendering
ModelRenderer::drawPath(painter, model, surface_rect.height());
}
if (longitudinal_control && sm.alive("radarState")) {
update_leads(radar_state, model.getPosition());
const auto &lead_two = radar_state.getLeadTwo();
if (lead_one.getStatus()) {
drawLead(painter, lead_one, lead_vertices[0], surface_rect);
}
if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) {
drawLead(painter, lead_two, lead_vertices[1], surface_rect);
}
}
if (s->scene.blindspot_ui) {
const bool left_blindspot = car_state.getLeftBlindspot();
const bool right_blindspot = car_state.getRightBlindspot();
drawBlindspot(painter, surface_rect, left_blindspot, right_blindspot);
}
drawLeadStatus(painter, surface_rect.height(), surface_rect.width());
painter.restore();
}
void ModelRendererSP::drawBlindspot(QPainter &painter, const QRect &surface_rect, bool left_blindspot, bool right_blindspot) {
if (left_blindspot && !left_blindspot_vertices.isEmpty()) {
QLinearGradient gradient(0, 0, surface_rect.width(), 0); // Horizontal gradient from left to right
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
painter.setBrush(gradient);
painter.drawPolygon(left_blindspot_vertices);
}
if (right_blindspot && !right_blindspot_vertices.isEmpty()) {
QLinearGradient gradient(surface_rect.width(), 0, 0, 0); // Horizontal gradient from right to left
gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha
gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha
painter.setBrush(gradient);
painter.drawPolygon(right_blindspot_vertices);
}
}
void ModelRendererSP::drawLeadStatus(QPainter &painter, int height, int width) {
@@ -121,19 +120,16 @@ void ModelRendererSP::drawLeadStatus(QPainter &painter, int height, int width) {
}
if (has_lead_one) {
drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1");
drawLeadStatusPosition(painter, lead_one, lead_vertices[0], height, width);
}
if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) {
drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2");
drawLeadStatusPosition(painter, lead_two, lead_vertices[1], height, width);
}
}
void ModelRendererSP::drawLeadStatusAtPosition(QPainter &painter,
const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos,
int height, int width,
const QString &label) {
void ModelRendererSP::drawLeadStatusPosition(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos, int height, int width) {
float d_rel = lead_data.getDRel();
float v_rel = lead_data.getVRel();
auto *s = uiState();
@@ -223,3 +219,36 @@ void ModelRendererSP::drawLeadStatusAtPosition(QPainter &painter,
painter.setPen(Qt::NoPen);
}
void ModelRendererSP::drawRainbowPath(QPainter &painter, const QRect &surface_rect) {
// Simple time-based animation
float time_offset = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count() / 1000.0f;
// simple linear gradient from bottom to top
QLinearGradient bg(0, surface_rect.height(), 0, 0);
// evenly spaced colors across the spectrum
// The animation shifts the entire spectrum smoothly
float animation_speed = 40.0f; // speed vroom vroom
float hue_offset = fmod(time_offset * animation_speed, 360.0f);
// 6-8 color stops for smooth transitions more color makes it laggy
const int num_stops = 7;
for (int i = 0; i < num_stops; i++) {
float position = static_cast<float>(i) / (num_stops - 1);
float hue = fmod(hue_offset + position * 360.0f, 360.0f);
float saturation = 0.9f;
float lightness = 0.6f;
// Alpha fades out towards the far end of the path
float alpha = 0.8f * (1.0f - position * 0.3f);
QColor color = QColor::fromHslF(hue / 360.0f, saturation, lightness, alpha);
bg.setColorAt(position, color);
}
painter.setBrush(bg);
painter.drawPolygon(track_vertices);
}
+6 -8
View File
@@ -13,17 +13,15 @@ class ModelRendererSP : public ModelRenderer {
public:
ModelRendererSP() = default;
void draw(QPainter &painter, const QRect &surface_rect);
private:
void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) override;
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &rect) override;
// Lead status display methods
void drawLeadStatus(QPainter &painter, int height, int width);
void drawLeadStatusAtPosition(QPainter &painter,
const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos,
int height, int width,
const QString &label);
void drawLeadStatusPosition(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data,
const QPointF &chevron_pos, int height, int width);
void drawBlindspot(QPainter &painter, const QRect &surface_rect, bool left_blindspot, bool right_blindspot);
void drawRainbowPath(QPainter &painter, const QRect &surface_rect);
QPolygonF left_blindspot_vertices;
QPolygonF right_blindspot_vertices;
+4
View File
@@ -122,3 +122,7 @@ std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::
return std::nullopt;
}
}
bool hasIntelligentCruiseButtonManagement(const cereal::CarParamsSP::Reader &car_params_sp) {
return car_params_sp.getIntelligentCruiseButtonManagementAvailable() && Params().getBool("IntelligentCruiseButtonManagement");
}
+1
View File
@@ -23,3 +23,4 @@ std::optional<QString> getParamIgnoringDefault(const std::string &param_name, co
QMap<QString, QVariantMap> loadPlatformList();
QStringList searchFromList(const QString &query, const QStringList &list);
std::optional<cereal::Event::Reader> loadCerealEvent(Params& params, const std::string& _param);
bool hasIntelligentCruiseButtonManagement(const cereal::CarParamsSP::Reader &car_params_sp);
@@ -390,7 +390,7 @@ class ButtonParamControlSP : public MultiButtonControlSP {
Q_OBJECT
public:
ButtonParamControlSP(const QString &param, const QString &title, const QString &desc, const QString &icon,
const std::vector<QString> &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon,
const std::vector<QString> &button_texts, const int minimum_button_width = 380, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon,
button_texts, minimum_button_width, inline_layout, advancedControl) {
key = param.toStdString();
int value = atoi(params.get(key).c_str());
+1 -1
View File
@@ -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
+1 -1
View File
@@ -18,7 +18,7 @@ if __name__ == "__main__":
while True:
print("setting alert update")
params.put_bool("UpdateAvailable", True)
r = open(os.path.join(BASEDIR, "RELEASES.md")).read()
r = open(os.path.join(BASEDIR, "CHANGELOG.md")).read()
r = r[:r.find('\n\n')] # Slice latest release notes
params.put("UpdaterNewReleaseNotes", r + "\n")
+1 -1
View File
@@ -188,7 +188,7 @@ def setup_offroad_alert(click, pm: PubMaster, scroll=None):
def setup_update_available(click, pm: PubMaster, scroll=None):
Params().put_bool("UpdateAvailable", True)
release_notes_path = os.path.join(BASEDIR, "RELEASES.md")
release_notes_path = os.path.join(BASEDIR, "CHANGELOG.md")
with open(release_notes_path) as file:
release_notes = file.read().split('\n\n', 1)[0]
Params().put("UpdaterNewReleaseNotes", release_notes + "\n")
+56 -56
View File
@@ -1778,62 +1778,6 @@ Warning: You are on a metered connection!</source>
<source>sunnypilot</source>
<translation>sunnypilot</translation>
</message>
<message>
<source>None</source>
<translation></translation>
</message>
<message>
<source>Fixed</source>
<translation></translation>
</message>
<message>
<source>Percent</source>
<translation></translation>
</message>
<message>
<source>Car
Only</source>
<translation></translation>
</message>
<message>
<source>Map
Only</source>
<translation></translation>
</message>
<message>
<source>Car
First</source>
<translation>
</translation>
</message>
<message>
<source>Map
First</source>
<translation>
</translation>
</message>
<message>
<source>Combined
Data</source>
<translation>
</translation>
</message>
<message>
<source>Off</source>
<translation></translation>
</message>
<message>
<source>Information</source>
<translation></translation>
</message>
<message>
<source>Warning</source>
<translation></translation>
</message>
<message>
<source>Assist</source>
<translation></translation>
</message>
</context>
<context>
<name>SettingsWindow</name>
@@ -2198,6 +2142,34 @@ Data</source>
<source>⦿ Combined: Use combined Speed Limit data from Car &amp; OpenStreetMaps</source>
<translation>⦿ 결합: 차량 OpenStreetMaps의 </translation>
</message>
<message>
<source>Car
Only</source>
<translation></translation>
</message>
<message>
<source>Map
Only</source>
<translation></translation>
</message>
<message>
<source>Car
First</source>
<translation>
</translation>
</message>
<message>
<source>Map
First</source>
<translation>
</translation>
</message>
<message>
<source>Combined
Data</source>
<translation>
</translation>
</message>
</context>
<context>
<name>SpeedLimitSettings</name>
@@ -2245,6 +2217,34 @@ Data</source>
<source>⦿ Assist: Adjusts the vehicle&apos;s cruise speed based on the current road&apos;s speed limit when operating the +/- buttons.</source>
<translation>⦿ : +/- .</translation>
</message>
<message>
<source>None</source>
<translation></translation>
</message>
<message>
<source>Fixed</source>
<translation></translation>
</message>
<message>
<source>Percent</source>
<translation></translation>
</message>
<message>
<source>Off</source>
<translation></translation>
</message>
<message>
<source>Information</source>
<translation></translation>
</message>
<message>
<source>Warning</source>
<translation></translation>
</message>
<message>
<source>Assist</source>
<translation></translation>
</message>
</context>
<context>
<name>SshControl</name>
-1
View File
@@ -60,7 +60,6 @@ typedef struct UIScene {
cereal::PandaState::PandaType pandaType;
cereal::LongitudinalPersonality personality;
cereal::LongitudinalPlanSP::AccelerationPersonality accel_personality;
float light_sensor = -1;
bool started, ignition, is_metric, recording_audio;
+21
View File
@@ -5,6 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from enum import IntEnum
import hashlib
PARAMS_UPDATE_PERIOD = 3 # seconds
@@ -16,3 +17,23 @@ def get_file_hash(path: str) -> str:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
class IntEnumBase(IntEnum):
@classmethod
def min(cls):
return min(cls)
@classmethod
def max(cls):
return max(cls)
def get_sanitize_int_param(key: str, min_val: int, max_val: int, params) -> int:
val: int = params.get(key, return_default=True)
clipped_val = max(min_val, min(max_val, val))
if clipped_val != val:
params.put(key, clipped_val)
return clipped_val
+1
View File
@@ -0,0 +1 @@
#define SUNNYPILOT_VERSION "2025.001.000"
@@ -8,8 +8,12 @@ from abc import abstractmethod, ABC
import cereal.messaging as messaging
from openpilot.common.params import Params
from openpilot.common.constants import CV
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.sunnypilot.navd.helpers import coordinate_from_param
MAX_SPEED_LIMIT = V_CRUISE_UNSET * CV.KPH_TO_MS
class BaseMapData(ABC):
def __init__(self):
@@ -46,9 +50,9 @@ class BaseMapData(ABC):
mapd_sp_send.valid = self.sm['liveLocationKalman'].gpsOK
live_map_data = mapd_sp_send.liveMapDataSP
live_map_data.speedLimitValid = bool(speed_limit > 0)
live_map_data.speedLimitValid = bool(MAX_SPEED_LIMIT > speed_limit > 0)
live_map_data.speedLimit = speed_limit
live_map_data.speedLimitAheadValid = bool(next_speed_limit > 0)
live_map_data.speedLimitAheadValid = bool(MAX_SPEED_LIMIT > next_speed_limit > 0)
live_map_data.speedLimitAhead = next_speed_limit
live_map_data.speedLimitAheadDistance = next_speed_limit_distance
live_map_data.roadName = self.get_current_road_name()
+1 -1
View File
@@ -37,7 +37,7 @@ class CarSpecificEventsSP:
# TODO-SP: add 1 m/s hysteresis
if CS.vEgo >= self.CP.minEnableSpeed:
self.low_speed_alert = False
if CS.gearShifter != GearShifter.drive:
if self.CP.minEnableSpeed >= 14.5 and CS.gearShifter != GearShifter.drive:
self.low_speed_alert = True
if self.low_speed_alert:
events.add(EventName.belowSteerSpeed)
+31
View File
@@ -11,6 +11,7 @@ from opendbc.car.interfaces import CarInterfaceBase
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import get_nn_model_path
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import set_speed_limit_assist_availability
import openpilot.system.sentry as sentry
@@ -66,6 +67,29 @@ def _initialize_torque_lateral_control(CI: CarInterfaceBase, CP: structs.CarPara
CI.configure_torque_tune(CP.carFingerprint, CP.lateralTuning)
def _cleanup_unsupported_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None) -> None:
if params is None:
params = Params()
if CP.steerControlType == structs.CarParams.SteerControlType.angle:
cloudlog.warning("SteerControlType is angle, cleaning up params")
params.remove("NeuralNetworkLateralControl")
params.remove("EnforceTorqueControl")
if not CP_SP.intelligentCruiseButtonManagementAvailable or CP.openpilotLongitudinalControl:
cloudlog.warning("ICBM not available or openpilot Longitudinal Control enabled, cleaning up params")
params.remove("IntelligentCruiseButtonManagement")
if not CP.openpilotLongitudinalControl and CP_SP.pcmCruiseSpeed:
cloudlog.warning("openpilot Longitudinal Control and ICBM not available, cleaning up params")
params.remove("DynamicExperimentalControl")
params.remove("CustomAccIncrementsEnabled")
params.remove("SmartCruiseControlVision")
params.remove("SmartCruiseControlMap")
set_speed_limit_assist_availability(CP, CP_SP, params)
def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None:
CP = CI.CP
CP_SP = CI.CP_SP
@@ -74,6 +98,7 @@ def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None:
nnlc_enabled = _initialize_neural_network_lateral_control(CP, CP_SP, params)
_initialize_intelligent_cruise_button_management(CP, CP_SP, params)
_initialize_torque_lateral_control(CI, CP, enforce_torque, nnlc_enabled)
_cleanup_unsupported_params(CP, CP_SP)
def initialize_params(params) -> list[dict[str, Any]]:
@@ -84,4 +109,10 @@ def initialize_params(params) -> list[dict[str, Any]]:
"HyundaiLongitudinalTuning"
])
# subaru
keys.extend([
"SubaruStopAndGo",
"SubaruStopAndGoManualParkingBrake",
])
return [{k: params.get(k, return_default=True)} for k in keys]
@@ -12,47 +12,159 @@ from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
TRIGGER_THRESHOLD = 30
GREEN_LIGHT_X_THRESHOLD = 30
LEAD_DEPART_DIST_THRESHOLD = 1.0
TRIGGER_TIMER_THRESHOLD = 0.3
class E2EStates:
INACTIVE = 0
ARMED = 1
CONSUMED = 2
class E2EAlertsHelper:
def __init__(self):
self._params = Params()
self._frame = -1
self.frame = -1
self.green_light_state = E2EStates.INACTIVE
self.prev_green_light_state = E2EStates.INACTIVE
self.lead_depart_state = E2EStates.INACTIVE
self.prev_lead_depart_state = E2EStates.INACTIVE
self.green_light_alert = False
self.green_light_alert_enabled = self._params.get_bool("GreenLightAlert")
self.lead_depart_alert = False
self.lead_depart_alert_enabled = self._params.get_bool("LeadDepartAlert")
self.green_light_trigger_timer = 0
self.lead_depart_trigger_timer = 0
self.last_lead_distance = -1
self.last_moving_frame = -1
self.allowed = False
self.last_allowed = False
self.has_lead = False
self.lead_depart_arm_timer = 0
self.lead_depart_confirmed_lead = False
self.lead_depart_armed = False
def _read_params(self) -> None:
if self._frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
self.green_light_alert_enabled = self._params.get_bool("GreenLightAlert")
self.lead_depart_alert_enabled = self._params.get_bool("LeadDepartAlert")
self._frame += 1
def update(self, sm: messaging.SubMaster, events_sp: EventsSP) -> None:
self._read_params()
if not (self.green_light_alert_enabled or self.lead_depart_alert_enabled):
return
def update_alert_trigger(self, sm: messaging.SubMaster):
CS = sm['carState']
CC = sm['carControl']
model_x = sm['modelV2'].position.x
max_idx = len(model_x) - 1
has_lead = sm['radarState'].leadOne.status
lead_vRel: float = sm['radarState'].leadOne.vRel
self.has_lead = sm['radarState'].leadOne.status
lead_dRel = sm['radarState'].leadOne.dRel
# Green light alert
self.green_light_alert = (self.green_light_alert_enabled and model_x[max_idx] > TRIGGER_THRESHOLD
and not has_lead and CS.standstill and not CS.gasPressed and not CC.enabled)
standstill = CS.standstill
moving = not standstill and CS.vEgo > 0.1
if moving:
self.last_moving_frame = self.frame
recent_moving = self.last_moving_frame == -1 or (self.frame - self.last_moving_frame) * DT_MDL < 2.0
self.allowed = not moving and not CS.gasPressed and not CC.enabled and not recent_moving
# Green Light Alert
green_light_trigger = False
if self.green_light_state == E2EStates.ARMED:
if model_x[max_idx] > GREEN_LIGHT_X_THRESHOLD:
self.green_light_trigger_timer += 1
else:
self.green_light_trigger_timer = 0
if self.green_light_trigger_timer * DT_MDL > TRIGGER_TIMER_THRESHOLD:
green_light_trigger = True
elif self.green_light_state != E2EStates.ARMED:
self.green_light_trigger_timer = 0
# Lead Departure Alert
self.lead_depart_alert = (self.lead_depart_alert_enabled and CS.standstill and model_x[max_idx] > 30
and has_lead and lead_vRel > 1 and not CS.gasPressed)
close_lead_valid = self.has_lead and lead_dRel < 8.0
if self.allowed and not self.last_allowed and close_lead_valid:
self.lead_depart_confirmed_lead = True
elif not self.allowed:
self.lead_depart_confirmed_lead = False
if self.allowed and self.lead_depart_confirmed_lead and close_lead_valid:
self.lead_depart_arm_timer += 1
if self.lead_depart_arm_timer * DT_MDL >= 1.0:
self.lead_depart_armed = True
else:
self.lead_depart_arm_timer = 0
self.lead_depart_armed = False
lead_depart_trigger = False
if self.lead_depart_state == E2EStates.ARMED:
if self.last_lead_distance == -1 or lead_dRel < self.last_lead_distance:
self.last_lead_distance = lead_dRel
if self.last_lead_distance != -1 and (lead_dRel - self.last_lead_distance > LEAD_DEPART_DIST_THRESHOLD):
self.lead_depart_trigger_timer += 1
else:
self.lead_depart_trigger_timer = 0
if self.lead_depart_trigger_timer * DT_MDL > TRIGGER_TIMER_THRESHOLD:
lead_depart_trigger = True
elif self.lead_depart_state != E2EStates.ARMED:
self.last_lead_distance = -1
self.lead_depart_trigger_timer = 0
self.last_allowed = self.allowed
return green_light_trigger, lead_depart_trigger
@staticmethod
def update_state_machine(state: int, enabled: bool, allowed: bool, triggered: bool) -> tuple[int, bool]:
if state != E2EStates.INACTIVE:
if not allowed or not enabled:
state = E2EStates.INACTIVE
else:
if state == E2EStates.ARMED:
if triggered:
state = E2EStates.CONSUMED
elif state == E2EStates.CONSUMED:
pass
elif state == E2EStates.INACTIVE:
if allowed and enabled:
state = E2EStates.ARMED
return state, triggered
def update(self, sm: messaging.SubMaster, events_sp: EventsSP) -> None:
self._read_params()
green_light_trigger, lead_depart_trigger = self.update_alert_trigger(sm)
self.prev_green_light_state = self.green_light_state
self.prev_lead_depart_state = self.lead_depart_state
self.green_light_state, self.green_light_alert = self.update_state_machine(
self.green_light_state,
self.green_light_alert_enabled,
self.allowed and not self.has_lead,
green_light_trigger
)
self.lead_depart_state, self.lead_depart_alert = self.update_state_machine(
self.lead_depart_state,
self.lead_depart_alert_enabled,
self.allowed and self.lead_depart_armed,
lead_depart_trigger
)
if self.green_light_alert or self.lead_depart_alert:
events_sp.add(custom.OnroadEventSP.EventName.e2eChime)
self.frame += 1
@@ -9,13 +9,15 @@ from cereal import custom
from openpilot.common.constants import CV
from openpilot.common.params import Params
TurnDirection = custom.ModelDataV2SP.TurnDirection
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
class LaneTurnController:
def __init__(self, desire_helper):
self.DH = desire_helper
self.turn_direction = custom.TurnDirection.none
self.turn_direction = TurnDirection.none
self.params = Params()
self.lane_turn_value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS
self.param_read_counter = 0
@@ -33,13 +35,13 @@ class LaneTurnController:
def update_lane_turn(self, blindspot_left: bool, blindspot_right: bool, left_blinker: bool, right_blinker: bool, v_ego: float) -> None:
if left_blinker and not right_blinker and v_ego < self.lane_turn_value and not blindspot_left:
self.turn_direction = custom.TurnDirection.turnLeft
self.turn_direction = TurnDirection.turnLeft
elif right_blinker and not left_blinker and v_ego < self.lane_turn_value and not blindspot_right:
self.turn_direction = custom.TurnDirection.turnRight
self.turn_direction = TurnDirection.turnRight
else:
self.turn_direction = custom.TurnDirection.none
self.turn_direction = TurnDirection.none
def get_turn_direction(self):
if not self.enabled:
return custom.TurnDirection.none
return TurnDirection.none
return self.turn_direction
@@ -17,20 +17,18 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolve
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.vibe_personality import VibePersonalityController
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
class LongitudinalPlannerSP:
def __init__(self, CP: structs.CarParams, mpc):
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
self.events_sp = EventsSP()
self.resolver = SpeedLimitResolver()
self.dec = DynamicExperimentalController(CP, mpc)
self.scc = SmartCruiseControl()
self.resolver = SpeedLimitResolver()
self.sla = SpeedLimitAssist(CP)
self.vibe_controller = VibePersonalityController()
self.sla = SpeedLimitAssist(CP, CP_SP)
self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None
self.source = LongitudinalPlanSource.cruise
self.e2e_alerts_helper = E2EAlertsHelper()
@@ -83,7 +81,6 @@ class LongitudinalPlannerSP:
self.events_sp.clear()
self.dec.update(sm)
self.e2e_alerts_helper.update(sm, self.events_sp)
self.vibe_controller.update()
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
plan_sp_send = messaging.new_message('longitudinalPlanSP')
@@ -4,10 +4,11 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from enum import IntEnum
from openpilot.sunnypilot import IntEnumBase
class Policy(IntEnum):
class Policy(IntEnumBase):
car_state_only = 0
map_data_only = 1
car_state_priority = 2
@@ -15,13 +16,13 @@ class Policy(IntEnum):
combined = 4
class OffsetType(IntEnum):
class OffsetType(IntEnumBase):
off = 0
fixed = 1
percentage = 2
class Mode(IntEnum):
class Mode(IntEnumBase):
off = 0
information = 1
warning = 2
@@ -5,7 +5,10 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import custom, car
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode
def compare_cluster_target(v_cruise_cluster: float, target_set_speed: float, is_metric: bool) -> tuple[bool, bool]:
@@ -17,3 +20,25 @@ def compare_cluster_target(v_cruise_cluster: float, target_set_speed: float, is_
req_minus = v_cruise_cluster_conv > target_set_speed_conv
return req_plus, req_minus
def set_speed_limit_assist_availability(CP: car.CarParams, CP_SP: custom.CarParamsSP, params: Params = None) -> bool:
if params is None:
params = Params()
is_release = params.get_bool("IsReleaseSpBranch")
disallow_in_release = CP.brand == "tesla" and is_release
always_disallow = CP.brand == "rivian"
allowed = True
if disallow_in_release or always_disallow:
allowed = False
if not CP.openpilotLongitudinalControl and CP_SP.pcmCruiseSpeed:
allowed = False
if not allowed:
if params.get("SpeedLimitMode", return_default=True) == SpeedLimitMode.assist:
params.put("SpeedLimitMode", int(SpeedLimitMode.warning))
return allowed
@@ -10,13 +10,13 @@ from cereal import custom, car
from openpilot.common.params import Params
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target, set_speed_limit_assist_availability
ButtonType = car.CarState.ButtonEvent.Type
EventNameSP = custom.OnroadEventSP.EventName
@@ -27,14 +27,18 @@ ACTIVE_STATES = (SpeedLimitAssistState.active, SpeedLimitAssistState.adapting)
ENABLED_STATES = (SpeedLimitAssistState.preActive, SpeedLimitAssistState.pending, *ACTIVE_STATES)
DISABLED_GUARD_PERIOD = 0.5 # secs.
PRE_ACTIVE_GUARD_PERIOD = 15 # secs. Time to wait after activation before considering temp deactivation signal.
# secs. Time to wait after activation before considering temp deactivation signal.
PRE_ACTIVE_GUARD_PERIOD = {
True: 15,
False: 5,
}
SPEED_LIMIT_CHANGED_HOLD_PERIOD = 1 # secs. Time to wait after speed limit change before switching to preActive.
LIMIT_MIN_ACC = -1.5 # m/s^2 Maximum deceleration allowed for limit controllers to provide.
LIMIT_MAX_ACC = 1.0 # m/s^2 Maximum acceleration allowed for limit controllers to provide while active.
LIMIT_MIN_SPEED = 8.33 # m/s, Minimum speed limit to provide as solution on limit controllers.
LIMIT_SPEED_OFFSET_TH = -1. # m/s Maximum offset between speed limit and current speed for adapting state.
V_CRUISE_UNSET = 255
V_CRUISE_UNSET = 255.
CRUISE_BUTTONS_PLUS = (ButtonType.accelCruise, ButtonType.resumeCruise)
CRUISE_BUTTONS_MINUS = (ButtonType.decelCruise, ButtonType.setCruise)
@@ -48,13 +52,15 @@ class SpeedLimitAssist:
a_ego: float
v_offset: float
def __init__(self, CP):
def __init__(self, CP: car.CarParams, CP_SP: custom.CarParamsSP):
self.params = Params()
self.CP = CP
self.CP_SP = CP_SP
self.frame = -1
self.long_engaged_timer = 0
self.pre_active_timer = 0
self.is_metric = self.params.get_bool("IsMetric")
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
self.long_enabled = False
self.long_enabled_prev = False
@@ -109,6 +115,16 @@ class SpeedLimitAssist:
def target_set_speed_confirmed(self) -> bool:
return bool(self.v_cruise_cluster_conv == self.target_set_speed_conv)
@property
def v_cruise_cluster_below_confirm_speed_threshold(self) -> bool:
return bool(self.v_cruise_cluster_conv < CONFIRM_SPEED_THRESHOLD[self.is_metric])
def update_active_event(self, events_sp: EventsSP) -> None:
if self.v_cruise_cluster_below_confirm_speed_threshold:
events_sp.add(EventNameSP.speedLimitChanged)
else:
events_sp.add(EventNameSP.speedLimitActive)
def get_v_target_from_control(self) -> float:
if self._has_speed_limit:
if self.pcm_op_long and self.is_enabled:
@@ -126,6 +142,7 @@ class SpeedLimitAssist:
def update_params(self) -> None:
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
self.is_metric = self.params.get_bool("IsMetric")
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
def update_car_state(self, CS: car.CarState) -> None:
@@ -175,7 +192,7 @@ class SpeedLimitAssist:
@property
def apply_confirm_speed_threshold(self) -> bool:
# below CST: always require user confirmation
if self.v_cruise_cluster_conv < CONFIRM_SPEED_THRESHOLD[self.is_metric]:
if self.v_cruise_cluster_below_confirm_speed_threshold:
return True
# at/above CST:
@@ -231,7 +248,7 @@ class SpeedLimitAssist:
self.state = SpeedLimitAssistState.inactive
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
elif self._has_speed_limit and self.v_offset < LIMIT_SPEED_OFFSET_TH:
self.state = SpeedLimitAssistState.adapting
@@ -241,7 +258,7 @@ class SpeedLimitAssist:
self.state = SpeedLimitAssistState.inactive
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
elif self.v_offset >= LIMIT_SPEED_OFFSET_TH:
self.state = SpeedLimitAssistState.active
@@ -251,7 +268,7 @@ class SpeedLimitAssist:
self._update_confirmed_state()
elif self.speed_limit_changed:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
# PRE_ACTIVE
elif self.state == SpeedLimitAssistState.preActive:
@@ -277,7 +294,7 @@ class SpeedLimitAssist:
self._update_confirmed_state()
elif self._has_speed_limit:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
else:
self.state = SpeedLimitAssistState.pending
@@ -303,7 +320,7 @@ class SpeedLimitAssist:
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
# PRE_ACTIVE
elif self.state == SpeedLimitAssistState.preActive:
@@ -317,7 +334,7 @@ class SpeedLimitAssist:
elif self.state == SpeedLimitAssistState.inactive:
if self.speed_limit_changed:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
elif self._update_non_pcm_long_confirmed_state():
self.state = SpeedLimitAssistState.active
@@ -333,7 +350,7 @@ class SpeedLimitAssist:
self.state = SpeedLimitAssistState.active
elif self._has_speed_limit:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
else:
self.state = SpeedLimitAssistState.inactive
@@ -351,15 +368,15 @@ class SpeedLimitAssist:
if self.is_active:
if self._state_prev not in ACTIVE_STATES:
events_sp.add(EventNameSP.speedLimitActive)
self.update_active_event(events_sp)
# only notify if we acquire a valid speed limit
# do not check has_speed_limit here
elif self._speed_limit != self.speed_limit_prev:
if self.speed_limit_prev <= 0:
events_sp.add(EventNameSP.speedLimitActive)
self.update_active_event(events_sp)
elif self.speed_limit_prev > 0 and self._speed_limit > 0:
events_sp.add(EventNameSP.speedLimitChanged)
self.update_active_event(events_sp)
def update(self, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float, v_cruise_cluster: float, speed_limit: float,
speed_limit_final_last: float, has_speed_limit: bool, distance: float, events_sp: EventsSP) -> None:
@@ -12,7 +12,7 @@ from openpilot.common.constants import CV
from openpilot.common.gps import get_gps_location_service
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD, get_sanitize_int_param
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy, OffsetType
@@ -42,6 +42,12 @@ class SpeedLimitResolver:
self.distance_solutions = {} # Store for distance to current speed limit start for different sources
self.policy = self.params.get("SpeedLimitPolicy", return_default=True)
self.policy = get_sanitize_int_param(
"SpeedLimitPolicy",
Policy.min().value,
Policy.max().value,
self.params
)
self._policy_to_sources_map = {
Policy.car_state_only: [SpeedLimitSource.car],
Policy.map_data_only: [SpeedLimitSource.map],
@@ -54,7 +60,12 @@ class SpeedLimitResolver:
self._reset_limit_sources(source)
self.is_metric = self.params.get_bool("IsMetric")
self.offset_type = self.params.get("SpeedLimitOffsetType", return_default=True)
self.offset_type = get_sanitize_int_param(
"SpeedLimitOffsetType",
OffsetType.min().value,
OffsetType.max().value,
self.params
)
self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True)
self.speed_limit = 0.
@@ -131,6 +142,7 @@ class SpeedLimitResolver:
self.limit_solutions[SpeedLimitSource.map] = speed_limit
self.distance_solutions[SpeedLimitSource.map] = 0.
# FIXME-SP: this is not working as expected
if 0. < next_speed_limit < self.v_ego:
adapt_time = (next_speed_limit - self.v_ego) / LIMIT_ADAPT_ACC
adapt_distance = self.v_ego * adapt_time + 0.5 * LIMIT_ADAPT_ACC * adapt_time ** 2
@@ -4,17 +4,22 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import custom
import pytest
from cereal import custom
from opendbc.car.car_helpers import interfaces
from opendbc.car.rivian.values import CAR as RIVIAN
from opendbc.car.tesla.values import CAR as TESLA
from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
@@ -30,16 +35,30 @@ SPEED_LIMITS = {
'freeway': 80 * CV.MPH_TO_MS, # 80 mph
}
DEFAULT_CAR = TOYOTA.TOYOTA_RAV4_TSS2
@pytest.fixture
def car_name(request):
return getattr(request, "param", DEFAULT_CAR)
@pytest.fixture(autouse=True)
def set_car_name_on_instance(request, car_name):
instance = getattr(request, "instance", None)
if instance:
instance.car_name = car_name
class TestSpeedLimitAssist:
def setup_method(self):
def setup_method(self, method):
self.params = Params()
self.reset_custom_params()
self.events_sp = EventsSP()
CI = self._setup_platform(TOYOTA.TOYOTA_RAV4_TSS2)
self.sla = SpeedLimitAssist(CI.CP)
self.sla.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)
CI = self._setup_platform(self.car_name)
self.sla = SpeedLimitAssist(CI.CP, CI.CP_SP)
self.sla.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.sla.pcm_op_long] / DT_MDL)
self.pcm_long_max_set_speed = PCM_LONG_REQUIRED_MAX_SET_SPEED[self.sla.is_metric][1] # use 80 MPH for now
self.speed_conv = CV.MS_TO_KPH if self.sla.is_metric else CV.MS_TO_MPH
@@ -51,10 +70,12 @@ class TestSpeedLimitAssist:
CP = CarInterface.get_non_essential_params(car_name)
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
CI = CarInterface(CP, CP_SP)
CI.CP.openpilotLongitudinalControl = True # always assume it's openpilot longitudinal
sunnypilot_interfaces.setup_interfaces(CI, self.params)
return CI
def reset_custom_params(self):
self.params.put("IsReleaseSpBranch", True)
self.params.put("SpeedLimitMode", int(Mode.assist))
self.params.put_bool("IsMetric", False)
self.params.put("SpeedLimitOffsetType", 0)
@@ -84,6 +105,22 @@ class TestSpeedLimitAssist:
assert not self.sla.is_active
assert V_CRUISE_UNSET == self.sla.get_v_target_from_control()
@pytest.mark.parametrize("car_name", [RIVIAN.RIVIAN_R1_GEN1, TESLA.TESLA_MODEL_Y], indirect=True)
def test_disallowed_brands(self, car_name):
"""
Speed Limit Assist is disabled for the following brands and conditions:
- All Tesla and is a release branch;
- All Rivian
"""
assert not self.sla.enabled
# stay disallowed even when the param may have changed from somewhere else
self.params.put("SpeedLimitMode", int(Mode.assist))
for _ in range(int(PARAMS_UPDATE_PERIOD / DT_MDL)):
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'],
SPEED_LIMITS['city'], True, 0, self.events_sp)
assert not self.sla.enabled
def test_disabled(self):
self.params.put("SpeedLimitMode", int(Mode.off))
for _ in range(int(10. / DT_MDL)):
@@ -114,7 +151,7 @@ class TestSpeedLimitAssist:
self.sla.state = SpeedLimitAssistState.preActive
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
for _ in range(int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)):
for _ in range(int(PRE_ACTIVE_GUARD_PERIOD[self.sla.pcm_op_long] / DT_MDL)):
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.inactive
@@ -1,113 +1,113 @@
import pytest
from cereal import log
from cereal import log, custom
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
class TurnDirection:
none = 0
turnLeft = 1
turnRight = 2
TurnDirection = custom.ModelDataV2SP.TurnDirection
@pytest.mark.parametrize("left_blinker,right_blinker,v_ego,blindspot_left,blindspot_right,expected", [
(True, False, 5, False, False, TurnDirection.turnLeft),
(False, True, 6, False, False, TurnDirection.turnRight),
(True, False, 9, False, False, TurnDirection.none),
(True, False, 7, True, False, TurnDirection.none),
(False, True, 6, False, True, TurnDirection.none),
(False, False, 5, False, False, TurnDirection.none),
(True, True, 5, False, False, TurnDirection.none),
(True, False, 5, False, False, TurnDirection.turnLeft),
(False, True, 6, False, False, TurnDirection.turnRight),
(True, False, 9, False, False, TurnDirection.none),
(True, False, 7, True, False, TurnDirection.none),
(False, True, 6, False, True, TurnDirection.none),
(False, False, 5, False, False, TurnDirection.none),
(True, True, 5, False, False, TurnDirection.none),
])
def test_lane_turn_desire_conditions(left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected):
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
assert controller.get_turn_direction() == expected
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego)
assert controller.get_turn_direction() == expected
def test_lane_turn_desire_disabled():
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = False
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(False, False, True, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = False
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(False, False, True, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
def test_lane_turn_overrides_lane_change():
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
# left turn desire
controller.update_lane_turn(False, False, True, False, 5)
assert controller.get_turn_direction() == TurnDirection.turnLeft
# right turn desire
controller.update_lane_turn(False, False, False, True, 6)
assert controller.get_turn_direction() == TurnDirection.turnRight
# no turn
controller.update_lane_turn(False, False, False, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
# left turn desire
controller.update_lane_turn(False, False, True, False, 5)
assert controller.get_turn_direction() == TurnDirection.turnLeft
# right turn desire
controller.update_lane_turn(False, False, False, True, 6)
assert controller.get_turn_direction() == TurnDirection.turnRight
# no turn
controller.update_lane_turn(False, False, False, False, 7)
assert controller.get_turn_direction() == TurnDirection.none
@pytest.mark.parametrize("v_ego,expected", [
(8.93, TurnDirection.turnLeft), # just below threshold
(8.96, TurnDirection.none), # above threshold
(8.95, TurnDirection.none), # just above threshold
(8.93, TurnDirection.turnLeft), # just below threshold
(8.96, TurnDirection.none), # above threshold
(8.95, TurnDirection.none), # just above threshold
])
def test_lane_turn_desire_speed_boundary(v_ego, expected):
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(False, True, True, False, v_ego)
assert controller.get_turn_direction() == expected
dh = DesireHelper()
controller = LaneTurnController(dh)
controller.enabled = True
controller.lane_turn_value = LANE_CHANGE_SPEED_MIN
controller.turn_direction = TurnDirection.none
controller.update_lane_turn(False, True, True, False, v_ego)
assert controller.get_turn_direction() == expected
class DummyCarState:
def __init__(self, vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=False, steeringTorque=0, brakePressed=False):
self.vEgo = vEgo
self.leftBlinker = leftBlinker
self.rightBlinker = rightBlinker
self.leftBlindspot = leftBlindspot
self.rightBlindspot = rightBlindspot
self.steeringPressed = steeringPressed
self.steeringTorque = steeringTorque
self.brakePressed = brakePressed
def __init__(self, vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=False, steeringTorque=0, brakePressed=False):
self.vEgo = vEgo
self.leftBlinker = leftBlinker
self.rightBlinker = rightBlinker
self.leftBlindspot = leftBlindspot
self.rightBlindspot = rightBlindspot
self.steeringPressed = steeringPressed
self.steeringTorque = steeringTorque
self.brakePressed = brakePressed
@pytest.fixture
def set_lane_turn_params():
params = Params()
params.put("LaneTurnDesire", True)
params.put("LaneTurnValue", 20.0)
params = Params()
params.put("LaneTurnDesire", True)
params.put("LaneTurnValue", 20.0)
@pytest.mark.parametrize("carstate, lateral_active, lane_change_prob, expected_desire", [
# Lane turn desire overrides lane change desire
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnLeft),
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnRight),
# Lane change desire only (no turn desires)
(DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft),
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight),
# No desire (inactive)
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none),
(DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire!
# Lane turn desire overrides lane change desire
(DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0,
log.Desire.turnLeft),
(DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0,
log.Desire.turnRight),
# Lane change desire only (no turn desires)
(DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft),
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False,
steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight),
# No desire (inactive)
(DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none),
(DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire!
])
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10):
dh.update(carstate, lateral_active, lane_change_prob)
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10):
dh.update(carstate, lateral_active, lane_change_prob)
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
@@ -1,74 +0,0 @@
"""
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import log, custom
import numpy as np
from openpilot.common.realtime import DT_MDL
from openpilot.common.params import Params
LongPersonality = log.LongitudinalPersonality
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
MAX_ACCEL_PROFILES = {
AccelPersonality.eco: [2.0, 1.99, 1.88, 1.10, .500, .292, .15, .10],
AccelPersonality.normal: [2.0, 2.00, 1.94, 1.22, .635, .33, .20, .16],
AccelPersonality.sport: [2.0, 2.00, 2.00, 1.85, .800, .54, .32, .22],
}
MAX_ACCEL_BREAKPOINTS = [0., 4., 6., 9., 16., 25., 30., 55.]
MIN_ACCEL_PROFILES = {
LongPersonality.relaxed: [-.0007, -.0007, -.07, -1.00, -1.00],
LongPersonality.standard: [-.0007, -.0007, -.08, -1.10, -1.10],
LongPersonality.aggressive: [-.0006, -.0007, -.09, -1.20, -1.20],
}
MIN_ACCEL_BREAKPOINTS = [0., 3.0, 11., 22., 50.]
class AccelPersonalityController:
def __init__(self):
self.params = Params()
self.frame = 0
self.accel_personality = AccelPersonality.normal
self.long_personality = LongPersonality.standard
def _update_from_params(self):
if self.frame % int(1. / DT_MDL) != 0:
return
accel_bytes = self.params.get('AccelPersonality')
if accel_bytes:
self.accel_personality = int(accel_bytes)
long_bytes = self.params.get('LongitudinalPersonality')
if long_bytes:
self.long_personality = int(long_bytes)
def is_enabled(self) -> bool:
return self.params.get_bool('VibeAccelPersonalityEnabled') or False
def set_accel_personality(self, personality: int) -> bool:
if personality not in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
return False
self.accel_personality = personality
self.params.put('AccelPersonality', str(personality))
return True
def cycle_accel_personality(self) -> int:
personalities = [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]
idx = personalities.index(self.accel_personality)
next_p = personalities[(idx + 1) % len(personalities)]
self.set_accel_personality(next_p)
return int(next_p)
def get_accel_limits(self, v_ego: float) -> tuple[float, float] | None:
if not self.is_enabled():
return None
self._update_from_params()
max_a = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self.accel_personality])
min_a = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[self.long_personality])
return float(min_a), float(max_a)
def update(self):
self.frame += 1
@@ -1,46 +0,0 @@
"""
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import log
import numpy as np
from openpilot.common.realtime import DT_MDL
from openpilot.common.params import Params
LongPersonality = log.LongitudinalPersonality
FOLLOW_PROFILES = {
LongPersonality.relaxed: [1.55, 1.65, 1.65, 1.80, 1.80],
LongPersonality.standard: [1.35, 1.45, 1.45, 1.55, 1.55],
LongPersonality.aggressive: [1.05, 1.10, 1.10, 1.25, 1.35],
}
FOLLOW_BREAKPOINTS = [0., 6., 18., 20., 36.]
class FollowPersonalityController:
def __init__(self):
self.params = Params()
self.frame = 0
self.long_personality = LongPersonality.standard
def _update_from_params(self):
if self.frame % int(1. / DT_MDL) != 0:
return
long_bytes = self.params.get('LongitudinalPersonality')
if long_bytes:
self.long_personality = int(long_bytes)
def is_enabled(self) -> bool:
return self.params.get_bool('VibeFollowPersonalityEnabled') or False
def get_follow_distance_multiplier(self, v_ego: float) -> float | None:
if not self.is_enabled():
return None
self._update_from_params()
return float(np.interp(v_ego, FOLLOW_BREAKPOINTS, FOLLOW_PROFILES[self.long_personality]))
def update(self):
self.frame += 1
@@ -1,314 +0,0 @@
import pytest
# Import the actual modules
from cereal import log, custom
from openpilot.common.realtime import DT_MDL
# Import the enums we need for testing
LongPersonality = log.LongitudinalPersonality
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
class MockParams:
"""Simple mock for Params class"""
def __init__(self):
self.data = {}
self.bool_data = {
'VibePersonalityEnabled': True,
'VibeAccelPersonalityEnabled': True,
'VibeFollowPersonalityEnabled': True
}
def get(self, key, encoding=None):
return self.data.get(key)
def get_bool(self, key):
return self.bool_data.get(key, True)
def put(self, key, value):
self.data[key] = value
def put_bool(self, key, value):
self.bool_data[key] = value
def reset_mock(self):
self.call_count = 0
@property
def call_count(self):
return getattr(self, '_call_count', 0)
@call_count.setter
def call_count(self, value):
self._call_count = value
@pytest.fixture
def mock_params():
"""Create mock params instance"""
return MockParams()
@pytest.fixture
def controller(mock_params, monkeypatch):
"""Create controller instance with mocked Params"""
# Patch the Params import in the controller module
monkeypatch.setattr('openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.vibe_personality.Params',
lambda: mock_params)
from openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.vibe_personality import VibePersonalityController
return VibePersonalityController()
class TestVibePersonalityController:
def test_initialization(self, controller):
"""Test controller initializes with correct defaults"""
assert controller.frame == 0
assert controller.accel_personality == AccelPersonality.normal
assert controller.long_personality == LongPersonality.standard
assert 'accel_personality' in controller.param_keys
assert 'long_personality' in controller.param_keys
def test_frame_increment(self, controller):
"""Test frame counter increments correctly"""
initial_frame = controller.frame
controller.update()
assert controller.frame == initial_frame + 1
controller.update()
assert controller.frame == initial_frame + 2
def test_parameter_reading_throttled(self, controller, mock_params):
"""Test parameters are only read every DT_MDL frames"""
# Track calls manually
original_get = mock_params.get
call_count = 0
def counting_get(*args, **kwargs):
nonlocal call_count
call_count += 1
return original_get(*args, **kwargs)
mock_params.get = counting_get
# First call should read params (frame 0)
controller._update_from_params()
# Reset counter
call_count = 0
# Advance frame but not to threshold
controller.frame = 5 # Less than int(1/DT_MDL)
controller._update_from_params()
assert call_count == 0 # Should not read params
# Advance to threshold
controller.frame = int(1. / DT_MDL) # Equal to threshold
controller._update_from_params()
assert call_count >= 2 # Should read both personality params
def test_accel_personality_management(self, controller, mock_params):
"""Test acceleration personality setting and cycling"""
# Test setting valid personality
assert controller.set_accel_personality(AccelPersonality.eco)
assert controller.accel_personality == AccelPersonality.eco
assert controller.set_accel_personality(AccelPersonality.sport)
assert controller.accel_personality == AccelPersonality.sport
# Test setting invalid personality
assert not controller.set_accel_personality(999)
assert controller.accel_personality == AccelPersonality.sport # Should remain unchanged
# Test cycling
controller.accel_personality = AccelPersonality.eco
next_personality = controller.cycle_accel_personality()
assert next_personality == AccelPersonality.normal # should cycle to normal
assert controller.accel_personality == AccelPersonality.normal
next_personality = controller.cycle_accel_personality()
assert next_personality == AccelPersonality.sport # should cycle to sport
next_personality = controller.cycle_accel_personality()
assert next_personality == AccelPersonality.eco # should cycle back to eco
def test_long_personality_management(self, controller, mock_params):
"""Test longitudinal personality setting and cycling"""
# Test setting valid personality
assert controller.set_long_personality(LongPersonality.relaxed)
assert controller.long_personality == LongPersonality.relaxed
assert controller.set_long_personality(LongPersonality.aggressive)
assert controller.long_personality == LongPersonality.aggressive
# Test setting invalid personality
assert not controller.set_long_personality(999)
assert controller.long_personality == LongPersonality.aggressive # Should remain unchanged
# Test cycling
controller.long_personality = LongPersonality.standard
next_personality = controller.cycle_long_personality()
assert next_personality == LongPersonality.aggressive # should cycle to aggressive
assert controller.long_personality == LongPersonality.aggressive
next_personality = controller.cycle_long_personality()
assert next_personality == LongPersonality.relaxed # should cycle to relaxed
next_personality = controller.cycle_long_personality()
assert next_personality == LongPersonality.standard # should cycle back to standard
def test_toggle_functions(self, controller, mock_params):
"""Test toggle functionality"""
# Set initial state to False
mock_params.bool_data['VibePersonalityEnabled'] = False
result = controller.toggle_personality()
assert result # Should toggle to True
assert mock_params.bool_data['VibePersonalityEnabled']
# Set initial state to True
mock_params.bool_data['VibeAccelPersonalityEnabled'] = True
result = controller.toggle_accel_personality()
assert not result # Should toggle to False
assert not mock_params.bool_data['VibeAccelPersonalityEnabled']
def test_enable_checks(self, controller, mock_params):
"""Test various enable state checks"""
# All enabled
mock_params.bool_data = {
'VibePersonalityEnabled': True,
'VibeAccelPersonalityEnabled': True,
'VibeFollowPersonalityEnabled': True
}
assert controller.is_enabled()
assert controller.is_accel_enabled()
assert controller.is_follow_enabled()
# Main toggle disabled
mock_params.bool_data['VibePersonalityEnabled'] = False
assert not controller.is_enabled()
assert not controller.is_accel_enabled()
assert not controller.is_follow_enabled()
def test_accel_limits_calculation(self, controller, mock_params):
"""Test acceleration limits calculation"""
# Enable all features through mock_params bool_data
mock_params.bool_data = {
'VibePersonalityEnabled': True,
'VibeAccelPersonalityEnabled': True,
'VibeFollowPersonalityEnabled': True
}
# Test with different speeds and personalities
controller.accel_personality = 1 # normal
controller.long_personality = 1 # standard
limits = controller.get_accel_limits(10.0) # 10 m/s
assert limits is not None
min_a, max_a = limits
assert isinstance(min_a, float)
assert isinstance(max_a, float)
assert min_a < 0 # Should be negative (braking)
assert max_a > 0 # Should be positive (acceleration)
# Test with disabled controller
mock_params.bool_data['VibePersonalityEnabled'] = False
limits = controller.get_accel_limits(10.0)
assert limits is None
def test_follow_distance_multiplier(self, controller, mock_params):
"""Test following distance multiplier calculation"""
# Enable controller
mock_params.bool_data['VibePersonalityEnabled'] = True
mock_params.bool_data['VibeFollowPersonalityEnabled'] = True
# Test with different speeds and personalities
controller.long_personality = LongPersonality.relaxed
multiplier = controller.get_follow_distance_multiplier(15.0) # 15 m/s
assert multiplier is not None
assert isinstance(multiplier, float)
assert multiplier > 0
# Test with different personality - aggressive should have shorter distance
controller.long_personality = LongPersonality.aggressive
aggressive_multiplier = controller.get_follow_distance_multiplier(15.0)
assert aggressive_multiplier is not None
assert aggressive_multiplier < multiplier # Aggressive should have shorter distance
# Test with disabled controller
mock_params.bool_data['VibeFollowPersonalityEnabled'] = False
multiplier = controller.get_follow_distance_multiplier(15.0)
assert multiplier is None
def test_personality_differences(self, controller, mock_params):
"""Test that different personalities actually produce different values"""
# Enable controller
mock_params.bool_data['VibePersonalityEnabled'] = True
mock_params.bool_data['VibeAccelPersonalityEnabled'] = True
mock_params.bool_data['VibeFollowPersonalityEnabled'] = True
# Test acceleration differences - sport should have higher max acceleration than eco
controller.accel_personality = AccelPersonality.eco
eco_limits = controller.get_accel_limits(20.0)
controller.accel_personality = AccelPersonality.sport
sport_limits = controller.get_accel_limits(20.0)
assert sport_limits[1] > eco_limits[1] # Sport should have higher max acceleration
# Test following distance differences - relaxed should have longer distance than aggressive
controller.long_personality = LongPersonality.relaxed
relaxed_dist = controller.get_follow_distance_multiplier(20.0)
controller.long_personality = LongPersonality.aggressive
aggressive_dist = controller.get_follow_distance_multiplier(20.0)
assert relaxed_dist > aggressive_dist # Relaxed should have longer following distance
def test_reset(self, controller):
"""Test reset functionality"""
# Change some values
controller.accel_personality = AccelPersonality.sport
controller.long_personality = LongPersonality.relaxed
controller.frame = 100
# Reset
controller.reset()
# Check defaults are restored
assert controller.accel_personality == AccelPersonality.normal
assert controller.long_personality == LongPersonality.standard
assert controller.frame == 0
def test_edge_cases(self, controller, mock_params):
"""Test edge cases and error handling"""
# Enable all features
mock_params.bool_data = {
'VibePersonalityEnabled': True,
'VibeAccelPersonalityEnabled': True,
'VibeFollowPersonalityEnabled': True
}
# Test with zero speed
limits = controller.get_accel_limits(0.0)
assert limits is not None
multiplier = controller.get_follow_distance_multiplier(0.0)
assert multiplier is not None
# Test with very high speed
limits = controller.get_accel_limits(100.0)
assert limits is not None
multiplier = controller.get_follow_distance_multiplier(100.0)
assert multiplier is not None
# Test interpolation works correctly
low_speed_limits = controller.get_accel_limits(5.0)
high_speed_limits = controller.get_accel_limits(50.0)
assert low_speed_limits[1] > high_speed_limits[1] # Max accel should decrease with speed
@@ -1,81 +0,0 @@
"""
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import log, custom
from openpilot.common.params import Params
from openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.accel_personality import AccelPersonalityController
from openpilot.sunnypilot.selfdrive.controls.lib.vibe_personality.follow_personality import FollowPersonalityController
LongPersonality = log.LongitudinalPersonality
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
class VibePersonalityController:
def __init__(self):
self.accel = AccelPersonalityController()
self.follow = FollowPersonalityController()
self.params = Params()
@property
def frame(self):
return self.accel.frame
@frame.setter
def frame(self, value):
self.accel.frame = self.follow.frame = value
@property
def accel_personality(self):
return self.accel.accel_personality
@property
def long_personality(self):
return self.accel.long_personality
@long_personality.setter
def long_personality(self, value):
self.accel.long_personality = self.follow.long_personality = value
def set_accel_personality(self, personality: int) -> bool:
return self.accel.set_accel_personality(personality)
def cycle_accel_personality(self) -> int:
return self.accel.cycle_accel_personality()
def set_long_personality(self, personality: int) -> bool:
if personality not in [LongPersonality.relaxed, LongPersonality.standard, LongPersonality.aggressive]:
return False
self.long_personality = personality
self.params.put('LongitudinalPersonality', str(personality))
return True
def cycle_long_personality(self) -> int:
personalities = [LongPersonality.relaxed, LongPersonality.standard, LongPersonality.aggressive]
idx = personalities.index(self.long_personality)
next_p = personalities[(idx + 1) % len(personalities)]
self.set_long_personality(next_p)
return int(next_p)
def is_enabled(self) -> bool:
main_enabled = self.params.get_bool('VibePersonalityEnabled') or False
return main_enabled and (self.accel.is_enabled() or self.follow.is_enabled())
def get_accel_limits(self, v_ego: float) -> tuple[float, float] | None:
main_enabled = self.params.get_bool('VibePersonalityEnabled') or False
if not (main_enabled and self.accel.is_enabled()):
return None
return self.accel.get_accel_limits(v_ego)
def get_follow_distance_multiplier(self, v_ego: float) -> float | None:
main_enabled = self.params.get_bool('VibePersonalityEnabled') or False
if not (main_enabled and self.follow.is_enabled()):
return None
return self.follow.get_follow_distance_multiplier(v_ego)
def update(self):
self.accel.update()
self.follow.update()
+14 -20
View File
@@ -4,13 +4,13 @@ from openpilot.common.constants import CV
from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \
NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType, wrong_car_mode_alert
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
VisualAlert = car.CarControl.HUDControl.VisualAlert
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
EventNameSP = custom.OnroadEventSP.EventName
@@ -33,6 +33,9 @@ def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messag
speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH
speed_limit_final_last = sm['longitudinalPlanSP'].speedLimit.resolver.speedLimitFinalLast
speed_limit_final_last_conv = round(speed_limit_final_last * speed_conv)
alert_1_str = ""
alert_2_str = ""
alert_size = AlertSize.none
if CP.openpilotLongitudinalControl and CP.pcmCruise:
# PCM long
@@ -40,25 +43,16 @@ def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messag
pcm_long_required_max = cst_low if speed_limit_final_last_conv < CONFIRM_SPEED_THRESHOLD[metric] else cst_high
pcm_long_required_max_set_speed_conv = round(pcm_long_required_max * speed_conv)
speed_unit = "km/h" if metric else "mph"
alert_1_str = "Speed Limit Assist: Activation Required"
alert_2_str = f"Manually change set speed to {pcm_long_required_max_set_speed_conv} {speed_unit} to activate"
else:
# Non PCM long
v_cruise_cluster = CS.vCruiseCluster * CV.KPH_TO_MS
req_plus, req_minus = compare_cluster_target(v_cruise_cluster, speed_limit_final_last, metric)
arrow_str = ""
if req_plus:
arrow_str = "RES/+"
elif req_minus:
arrow_str = "SET/-"
alert_2_str = f"Operate the {arrow_str} cruise control button to activate"
alert_size = AlertSize.mid
return Alert(
"Speed Limit Assist: Activation Required",
alert_1_str,
alert_2_str,
AlertStatus.normal, AlertSize.mid,
Priority.LOW, VisualAlert.none, AudibleAlert.none, .1)
AlertStatus.normal, alert_size,
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleLow, .1)
class EventsSP(EventsBase):
@@ -202,7 +196,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
"Automatically adjusting to the posted speed limit",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.),
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
},
EventNameSP.speedLimitChanged: {
@@ -210,7 +204,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
"Set speed changed",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.),
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
},
EventNameSP.speedLimitPreActive: {
@@ -222,7 +216,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
"Automatically adjusting to the last speed limit",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.),
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
},
EventNameSP.e2eChime: {
@@ -230,6 +224,6 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
"",
"",
AlertStatus.normal, AlertSize.none,
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 0.1),
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
},
}
+3 -1
View File
@@ -11,6 +11,8 @@
#include "common/swaglog.h"
#include "common/version.h"
#include "sunnypilot/common/version.h"
// ***** log metadata *****
kj::Array<capnp::word> logger_build_init_data() {
uint64_t wall_time = nanos_since_epoch();
@@ -19,7 +21,7 @@ kj::Array<capnp::word> logger_build_init_data() {
auto init = msg.initEvent().initInitData();
init.setWallTimeNanos(wall_time);
init.setVersion(COMMA_VERSION);
init.setVersion(SUNNYPILOT_VERSION);
init.setDirty(!getenv("CLEAN"));
init.setDeviceType(Hardware::get_device_type());
+1
View File
@@ -67,6 +67,7 @@ def manager_init() -> None:
params.put_bool("IsDevelopmentBranch", build_metadata.development_channel)
params.put_bool("IsTestedBranch", build_metadata.tested_channel)
params.put_bool("IsReleaseBranch", build_metadata.release_channel)
params.put_bool("IsReleaseSpBranch", build_metadata.release_sp_channel)
params.put("HardwareSerial", serial)
# set dongle id
+2 -2
View File
@@ -82,7 +82,7 @@ def set_consistent_flag(consistent: bool) -> None:
def parse_release_notes(basedir: str) -> bytes:
try:
with open(os.path.join(basedir, "RELEASES.md"), "rb") as f:
with open(os.path.join(basedir, "CHANGELOG.md"), "rb") as f:
r = f.read().split(b'\n\n', 1)[0] # Slice latest release notes
try:
return bytes(parse_markdown(r.decode("utf-8")), encoding="utf-8")
@@ -294,7 +294,7 @@ class Updater:
try:
branch = self.get_branch(basedir)
commit = self.get_commit_hash(basedir)[:7]
with open(os.path.join(basedir, "common", "version.h")) as f:
with open(os.path.join(basedir, "sunnypilot", "common", "version.h")) as f:
version = f.read().split('"')[1]
commit_unix_ts = run(["git", "show", "-s", "--format=%ct", "HEAD"], basedir).rstrip()
+13 -7
View File
@@ -10,11 +10,11 @@ from openpilot.common.basedir import BASEDIR
from openpilot.common.swaglog import cloudlog
from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date
RELEASE_SP_BRANCHES = ['release-c3', 'release']
RELEASE_SP_BRANCHES = ['release-c3', 'release', 'release-tizi', 'release-tici', 'release-tizi-staging', 'release-tici-staging']
TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new', 'staging']
MASTER_SP_BRANCHES = ['master']
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly']
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + RELEASE_SP_BRANCHES + TESTED_SP_BRANCHES
SP_BRANCH_MIGRATIONS = {
("tici", "staging-c3-new"): "staging-tici",
@@ -33,13 +33,13 @@ terms_version: str = "2"
def get_version(path: str = BASEDIR) -> str:
with open(os.path.join(path, "common", "version.h")) as _versionf:
with open(os.path.join(path, "sunnypilot", "common", "version.h")) as _versionf:
version = _versionf.read().split('"')[1]
return version
def get_release_notes(path: str = BASEDIR) -> str:
with open(os.path.join(path, "RELEASES.md")) as f:
with open(os.path.join(path, "CHANGELOG.md")) as f:
return f.read().split('\n\n', 1)[0]
@@ -96,7 +96,9 @@ class OpenpilotMetadata:
@property
def sunnypilot_remote(self) -> bool:
return self.git_normalized_origin in ("github.com/sunnypilot/sunnypilot",
"github.com/sunnypilot/openpilot")
"github.com/sunnypilot/openpilot",
"github.com/sunnyhaibin/sunnypilot",
"github.com/sunnyhaibin/openpilot")
@property
def git_normalized_origin(self) -> str:
@@ -120,6 +122,10 @@ class BuildMetadata:
def release_channel(self) -> bool:
return self.channel in RELEASE_BRANCHES
@property
def release_sp_channel(self) -> bool:
return self.channel in RELEASE_SP_BRANCHES
@property
def canonical(self) -> str:
return f"{self.openpilot.version}-{self.openpilot.git_commit}-{self.openpilot.build_style}"
@@ -146,7 +152,7 @@ class BuildMetadata:
return "staging"
elif self.master_channel:
return "master"
elif self.release_channel:
elif self.release_channel or self.release_sp_channel:
return "release"
else:
return "feature"
+3 -1
View File
@@ -14,6 +14,8 @@
#include "common/version.h"
#include "system/hardware/hw.h"
#include "sunnypilot/common/version.h"
namespace CommaApi2 {
// Base64 URL-safe character set (uses '-' and '_' instead of '+' and '/')
@@ -141,7 +143,7 @@ std::string httpGet(const std::string &url, long *response_code) {
// Handle headers
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "User-Agent: openpilot-" COMMA_VERSION);
headers = curl_slist_append(headers, "User-Agent: openpilot-" SUNNYPILOT_VERSION);
if (!token.empty()) {
headers = curl_slist_append(headers, ("Authorization: JWT " + token).c_str());
}
+3 -1
View File
@@ -10,6 +10,8 @@
#include "common/util.h"
#include "common/version.h"
#include "sunnypilot/common/version.h"
namespace {
const int BORDER_SIZE = 3;
@@ -119,7 +121,7 @@ void ConsoleUI::initWindows() {
// set the title bar
wbkgd(w[Win::Title], A_REVERSE);
mvwprintw(w[Win::Title], 0, 3, "sunnypilot replay %s", COMMA_VERSION);
mvwprintw(w[Win::Title], 0, 3, "sunnypilot replay %s", SUNNYPILOT_VERSION);
// show windows on the real screen
refresh();