From 6f089de1a7982372dbf64d547157b57ba98c02d2 Mon Sep 17 00:00:00 2001 From: FrogAi <91348155+FrogAi@users.noreply.github.com> Date: Fri, 7 Jun 2024 05:15:26 -0700 Subject: [PATCH] FrogPilot setup - Toggle panels --- .../toggle_icons/icon_always_on_lateral.png | Bin 0 -> 9723 bytes .../assets/toggle_icons/icon_conditional.png | Bin 0 -> 9965 bytes .../assets/toggle_icons/icon_device.png | Bin 0 -> 10538 bytes .../assets/toggle_icons/icon_green_light.png | Bin 0 -> 11651 bytes .../assets/toggle_icons/icon_lane.png | Bin 0 -> 22312 bytes .../assets/toggle_icons/icon_lateral_tune.png | Bin 0 -> 21482 bytes .../assets/toggle_icons/icon_light.png | Bin 0 -> 16028 bytes .../toggle_icons/icon_longitudinal_tune.png | Bin 0 -> 25120 bytes .../assets/toggle_icons/icon_mute.png | Bin 0 -> 9585 bytes .../assets/toggle_icons/icon_personality.png | Bin 0 -> 14244 bytes .../assets/toggle_icons/icon_speed_map.png | Bin 0 -> 25586 bytes .../assets/toggle_icons/icon_vtc.png | Bin 0 -> 12729 bytes .../assets/toggle_icons/quality_of_life.png | Bin 0 -> 15524 bytes .../ui/qt/offroad/control_settings.cc | 1201 +++++++++++++++++ .../ui/qt/offroad/control_settings.h | 82 ++ .../ui/qt/offroad/personalities_info.txt | 23 + .../ui/qt/offroad/vehicle_settings.cc | 307 +++++ .../ui/qt/offroad/vehicle_settings.h | 48 + .../ui/qt/offroad/visual_settings.cc | 452 +++++++ .../frogpilot/ui/qt/offroad/visual_settings.h | 43 + .../ui/qt/widgets/frogpilot_controls.cc | 38 + .../ui/qt/widgets/frogpilot_controls.h | 705 ++++++++++ selfdrive/ui/SConscript | 5 +- selfdrive/ui/qt/offroad/settings.cc | 65 +- selfdrive/ui/qt/offroad/settings.h | 20 + selfdrive/ui/qt/widgets/controls.h | 4 + selfdrive/ui/qt/widgets/scrollview.cc | 4 + selfdrive/ui/qt/widgets/scrollview.h | 4 + selfdrive/ui/ui.cc | 1 + selfdrive/ui/ui.h | 1 + 30 files changed, 3001 insertions(+), 2 deletions(-) create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_always_on_lateral.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_conditional.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_device.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_green_light.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_lane.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_lateral_tune.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_light.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_longitudinal_tune.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_mute.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_personality.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_speed_map.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/icon_vtc.png create mode 100644 selfdrive/frogpilot/assets/toggle_icons/quality_of_life.png create mode 100644 selfdrive/frogpilot/ui/qt/offroad/control_settings.cc create mode 100644 selfdrive/frogpilot/ui/qt/offroad/control_settings.h create mode 100644 selfdrive/frogpilot/ui/qt/offroad/personalities_info.txt create mode 100644 selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.cc create mode 100644 selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.h create mode 100644 selfdrive/frogpilot/ui/qt/offroad/visual_settings.cc create mode 100644 selfdrive/frogpilot/ui/qt/offroad/visual_settings.h diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_always_on_lateral.png b/selfdrive/frogpilot/assets/toggle_icons/icon_always_on_lateral.png new file mode 100644 index 0000000000000000000000000000000000000000..1e55e3feb958850c75f576a8854224c2becc4cee GIT binary patch literal 9723 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≻}>t1hIjeO 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 literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_device.png b/selfdrive/frogpilot/assets/toggle_icons/icon_device.png new file mode 100644 index 0000000000000000000000000000000000000000..2275347c45db570b7b330c1ac80f35ca34089ff1 GIT binary patch literal 10538 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 XD2PzJZUk`n5uv86tyHdXKji-a%2FE( literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_green_light.png b/selfdrive/frogpilot/assets/toggle_icons/icon_green_light.png new file mode 100644 index 0000000000000000000000000000000000000000..cb47d7a81d4d58569847683c23d66fdb505c13b2 GIT binary patch literal 11651 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 UQ0+W{scOUnd3Cum8B^4M0XjK|ApigX literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_lane.png b/selfdrive/frogpilot/assets/toggle_icons/icon_lane.png new file mode 100644 index 0000000000000000000000000000000000000000..eea1440c7013cbf61c5cf3a95ebe8021aa160c61 GIT binary patch literal 22312 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

K~eJqkN^qqc=?e_4jZ~u@@8|P154P zZhQAyWZEMKhc96JvuK%|3py)b(UG8pdN4))S?RRj1SYIA^t~y}$EQFh7@7g;)ks30 zShdCwT)9VrK<6d(&O@mO+zOud;wE7Yu3#=BM-Q8=+$Ke(M>5k6MdDeB#gNlWEDR#StQ3G!kcY6UM=57VR z+7S|m{us9|cq(w`$~g6H-M%;@_zFoUiDZgu09%(FsNtN8VCKFU-#QF*#8-(a8tLkG z)q)p>%X-ZQoYoS8Pa3nHFKwbU(ZzR}7 zbxhLbN(VZm@rGgXmd#i!cF|+9?2rV93*`G|dph9e=0+igs5tpGsJKY&FQ5VDc}#a4 zHLHv?KmP~x|1L-B;)Os=6@wxYDf%C8hxie_Innz>%eYH_=J8!{bO9Hy>r)~6{>kCo zH*Z3ghQaqa0{Nu*Dr8M!f_+!0a%%?;^kS2tCwt4g6F@vVmq}h#{zt}GV_j(gHZ=Pw zU|$Fd8*8s8&`e^>nMp=+fe2S9c=yeVwczuq^AOrnZ-Z#?(NSO?dZX;S(m@`(2hk6% zKqAFX3cvomdO26zkZd^^!>6XwrQ`+m!y6AN3WTj+35|Vp>USG*$ntlkpfAJ%49{85 z^z<>&^Y-X_e`ACbRbw{y+Vfpv*BS*#HH7lkU@dmE9!uw)V6^-P!ku)V?0oL)w@Y;U zsStS-sra@mMEu=3x_qONki*R<65`?kN*pvq9u`?dy9fTYx+kNqawoMQG1iF%);RN@ zq+NVrK*!ER1px}@~l-iVE-j2=1Pd~NfMF|sVo%+5c5 z{>-mpqBJdM^(?Bo8NsQMc;|vgu^W`4Pasts`-C2LO;Rar*9s$3!7xmSjp4zwzwDa( z`t|EJVEFOfr1~OSTv868L}j#sSNR-R#Ch8 zj|GcYe-~6`iaPV^Ut==fyDe^W@_85B*viTR1aTOQ~P;+3$-vfsmbBqL%yS9>aMBA*OQ=zq_F=Zl;l#0OfR|hV)Q1*+XQB zDBfdZgS9g560yqm>3v?euLPi+`bwy&6;4WBf>Ya>CkRSaMPTu&B8&@`O&6@={yipy ztGga7-cf&W@B-0($zr`~V>-Orm<1MZ%*x9fb-xbQDSb$_8O#ApWLM~>0U7E4??Xmk aKm}2e!8+IOQ2_KU!~kZjQ>FDF=6?V|f}$b- literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_lateral_tune.png b/selfdrive/frogpilot/assets/toggle_icons/icon_lateral_tune.png new file mode 100644 index 0000000000000000000000000000000000000000..3dcd9c7fc5d7052d271311b069134d671ad1d217 GIT binary patch literal 21482 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 G7W*$G(3|1_ literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_light.png b/selfdrive/frogpilot/assets/toggle_icons/icon_light.png new file mode 100644 index 0000000000000000000000000000000000000000..043b5c03d43c232bc35c14569ac7ff79495af085 GIT binary patch literal 16028 zcma)jWmr{f7wv{kw{)j;Nw;pg8wEi^K?x}dK@jOiQWOD^21P;%DQPL?(1?V9G}7JN zxxf4O{kR zf#GSSu7oK2k8u@&U`1%ED4O_MZCK*V-<~{bzAo;_uAC>%ujFv!Ixm&hL2~j>B^5)u zTq0r>J1%9)g5Q+H5yjk`hV(Cqm$Zs`7OyNWp0WI=@J6oTm%{Si>fX)%-~Jobzb5`= zefwqguVLJMyd96+CM6{$<3mY_f6&X9FE_?N+jjN-_@Vto`W`pOjjmq#iqE?XbPqQz zYMk#fP*8Zjjg5UwyFK#f&!w`dsgH${Qk%EhT+;267NRf=acgsPb3S%<_Mw#2FJHcV zq!W9jV~aR6OUM3+qD5W2d-tx`f_L0i1mQ2xhgpcgF}U@Jl_(nvQ@{qN4N>0;cX$;< zhu6**Tv}PVM@r!H&e_>n`G)>j%Np@n2+^nW`}=Hg4<;-kJ06nAGlgw|FG9F!!9

    }B+ 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 literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_longitudinal_tune.png b/selfdrive/frogpilot/assets/toggle_icons/icon_longitudinal_tune.png new file mode 100644 index 0000000000000000000000000000000000000000..7648a38b2b6bcc7457bacbda7c37d9190b0dc3e4 GIT binary patch literal 25120 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 literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/icon_vtc.png b/selfdrive/frogpilot/assets/toggle_icons/icon_vtc.png new file mode 100644 index 0000000000000000000000000000000000000000..269ba1ea586b25589ca54bbe5c37875526ea13ba GIT binary patch literal 12729 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 literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/assets/toggle_icons/quality_of_life.png b/selfdrive/frogpilot/assets/toggle_icons/quality_of_life.png new file mode 100644 index 0000000000000000000000000000000000000000..5130f152755be6f5548f8bd81f5dd6cf9f9bdf70 GIT binary patch literal 15524 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 literal 0 HcmV?d00001 diff --git a/selfdrive/frogpilot/ui/qt/offroad/control_settings.cc b/selfdrive/frogpilot/ui/qt/offroad/control_settings.cc new file mode 100644 index 0000000000..90779d974d --- /dev/null +++ b/selfdrive/frogpilot/ui/qt/offroad/control_settings.cc @@ -0,0 +1,1201 @@ +#include +#include + +#include "selfdrive/frogpilot/ui/qt/offroad/control_settings.h" + +bool checkCommaNNFFSupport(const std::string &carFingerprint) { + std::ifstream file("../car/torque_data/neural_ff_weights.json"); + for (std::string line; std::getline(file, line);) { + if (line.find(carFingerprint) != std::string::npos) { + std::cout << "comma's NNFF supports fingerprint: " << carFingerprint << std::endl; + return true; + } + } + return false; +} + +bool checkNNFFLogFileExists(const std::string &carFingerprint) { + for (const auto &entry : std::filesystem::directory_iterator("../car/torque_data/lat_models")) { + if (entry.path().filename().string().find(carFingerprint) == 0) { + std::cout << "NNFF supports fingerprint: " << entry.path().filename() << std::endl; + return true; + } + } + return false; +} + +FrogPilotControlsPanel::FrogPilotControlsPanel(SettingsWindow *parent) : FrogPilotListWidget(parent) { + std::string branch = params.get("GitBranch"); + isRelease = branch == "FrogPilot"; + + const std::vector> controlToggles { + {"AlwaysOnLateral", tr("Always on Lateral"), tr("Maintain openpilot lateral control when the brake or gas pedals are used.\n\nDeactivation occurs only through the 'Cruise Control' button."), "../frogpilot/assets/toggle_icons/icon_always_on_lateral.png"}, + {"AlwaysOnLateralLKAS", tr("Control Via LKAS Button"), tr("Enable or disable 'Always On Lateral' by clicking your 'LKAS' button."), ""}, + {"AlwaysOnLateralMain", tr("Enable On Cruise Main"), tr("Enable 'Always On Lateral' by clicking your 'Cruise Control' button without requiring openpilot to be enabled first."), ""}, + {"PauseAOLOnBrake", tr("Pause On Brake Below"), tr("Pause 'Always On Lateral' when the brake pedal is being pressed below the set speed."), ""}, + {"HideAOLStatusBar", tr("Hide the Status Bar"), tr("Don't use the status bar for 'Always On Lateral'."), ""}, + + {"ConditionalExperimental", tr("Conditional Experimental Mode"), tr("Automatically switches to 'Experimental Mode' under predefined conditions."), "../frogpilot/assets/toggle_icons/icon_conditional.png"}, + {"CECurves", tr("Curve Detected Ahead"), tr("Switch to 'Experimental Mode' when a curve is detected."), ""}, + {"CELead", tr("Lead Detected Ahead"), tr("Switch to 'Experimental Mode' when a slower or stopped lead vehicle is detected ahead."), ""}, + {"CENavigation", tr("Navigation Based"), tr("Switch to 'Experimental Mode' based on navigation data. (i.e. Intersections, stop signs, upcoming turns, etc.)"), ""}, + {"CEStopLights", tr("Stop Lights and Stop Signs"), tr("Switch to 'Experimental Mode' when a stop light or stop sign is detected."), ""}, + {"CESignal", tr("Turn Signal When Below Highway Speeds"), tr("Switch to 'Experimental Mode' when using turn signals below highway speeds to help assist with turns."), ""}, + {"HideCEMStatusBar", tr("Hide the Status Bar"), tr("Don't use the status bar for 'Conditional Experimental Mode'."), ""}, + + {"DeviceManagement", tr("Device Management"), tr("Tweak your device's behaviors to your personal preferences."), "../frogpilot/assets/toggle_icons/icon_device.png"}, + {"DeviceShutdown", tr("Device Shutdown Timer"), tr("Configure how quickly the device shuts down after going offroad."), ""}, + {"NoLogging", tr("Disable Logging"), tr("Turn off all data tracking to enhance privacy or reduce thermal load."), ""}, + {"NoUploads", tr("Disable Uploads"), tr("Turn off all data uploads to comma's servers."), ""}, + {"IncreaseThermalLimits", tr("Increase Thermal Safety Limit"), tr("Allow the device to run at a temperature above comma's recommended thermal limits."), ""}, + {"LowVoltageShutdown", tr("Low Voltage Shutdown Threshold"), tr("Automatically shut the device down when your battery reaches a specific voltage level to prevent killing your battery."), ""}, + {"OfflineMode", tr("Offline Mode"), tr("Allow the device to be offline indefinitely."), ""}, + + {"DrivingPersonalities", tr("Driving Personalities"), tr("Manage the driving behaviors of comma's 'Personality Profiles'."), "../frogpilot/assets/toggle_icons/icon_personality.png"}, + {"CustomPersonalities", tr("Customize Personalities"), tr("Customize the driving personality profiles to your driving style."), ""}, + {"TrafficPersonalityProfile", tr("Traffic Personality"), tr("Customize the 'Traffic' personality profile."), "../frogpilot/assets/other_images/traffic.png"}, + {"TrafficFollow", tr("Following Distance"), tr("Set the minimum following distance when using 'Traffic Mode'. Your following distance will dynamically adjust between this distance and the following distance from the 'Aggressive' profile when driving between 0 and %1.\n\nFor example:\n\nTraffic Mode: 0.5s\nAggressive: 1.0s\n\n0%2 = 0.5s\n%3 = 0.75s\n%1 = 1.0s"), ""}, + {"TrafficJerkAcceleration", tr("Acceleration/Deceleration Response Offset"), tr("Customize the response rate for acceleration when using 'Traffic Mode'."), ""}, + {"TrafficJerkDanger", tr("Danger Zone Response Offset"), tr("Customize the response rate for the danger zone factor when using the 'Traffic' personality."), ""}, + {"TrafficJerkSpeed", tr("Speed Control Response Offset"), tr("Customize the response rate for keeping your speed (including braking) when using 'Traffic Mode'."), ""}, + {"ResetTrafficPersonality", tr("Reset Settings"), tr("Reset the values for the 'Traffic Mode' personality back to stock."), ""}, + {"AggressivePersonalityProfile", tr("Aggressive Personality"), tr("Customize the 'Aggressive' personality profile."), "../frogpilot/assets/other_images/aggressive.png"}, + {"AggressiveFollow", tr("Following Distance"), tr("Set the 'Aggressive' personality following distance. Represents seconds to follow behind the lead vehicle.\n\nStock: 1.25 seconds."), ""}, + {"AggressiveJerkAcceleration", tr("Acceleration/Deceleration Response Offset"), tr("Customize the response rate for acceleration when using the 'Aggressive' personality."), ""}, + {"AggressiveJerkDanger", tr("Danger Zone Response Offset"), tr("Customize the response rate for the danger zone factor when using the 'Aggressive' personality."), ""}, + {"AggressiveJerkSpeed", tr("Speed Control Response Offset"), tr("Customize the response rate for keeping your speed (including braking) when using the 'Aggressive' personality."), ""}, + {"ResetAggressivePersonality", tr("Reset Settings"), tr("Reset the values for the 'Aggressive' personality back to stock."), ""}, + {"StandardPersonalityProfile", tr("Standard Personality"), tr("Customize the 'Standard' personality profile."), "../frogpilot/assets/other_images/standard.png"}, + {"StandardFollow", tr("Following Distance"), tr("Set the 'Standard' personality following distance. Represents seconds to follow behind the lead vehicle.\n\nStock: 1.45 seconds."), ""}, + {"StandardJerkAcceleration", tr("Acceleration/Deceleration Response Offset"), tr("Customize the response rate for acceleration when using the 'Standard' personality."), ""}, + {"StandardJerkDanger", tr("Danger Zone Response Offset"), tr("Customize the response rate for the danger zone factor when using the 'Standard' personality."), ""}, + {"StandardJerkSpeed", tr("Speed Control Response Offset"), tr("Customize the response rate for keeping your speed (including braking) when using the 'Standard' personality."), ""}, + {"ResetStandardPersonality", tr("Reset Settings"), tr("Reset the values for the 'Standard' personality back to stock."), ""}, + {"RelaxedPersonalityProfile", tr("Relaxed Personality"), tr("Customize the 'Relaxed' personality profile."), "../frogpilot/assets/other_images/relaxed.png"}, + {"RelaxedFollow", tr("Following Distance"), tr("Set the 'Relaxed' personality following distance. Represents seconds to follow behind the lead vehicle.\n\nStock: 1.75 seconds."), ""}, + {"RelaxedJerkAcceleration", tr("Acceleration/Deceleration Response Offset"), tr("Customize the response rate for acceleration when using the 'Relaxed' personality."), ""}, + {"RelaxedJerkDanger", tr("Danger Zone Response Offset"), tr("Customize the response rate for the danger zone factor when using the 'Relaxed' personality."), ""}, + {"RelaxedJerkSpeed", tr("Speed Control Response Offset"), tr("Customize the response rate for keeping your speed (including braking) when using the 'Relaxed' personality."), ""}, + {"ResetRelaxedPersonality", tr("Reset Settings"), tr("Reset the values for the 'Relaxed' personality back to stock."), ""}, + {"OnroadDistanceButton", tr("Onroad Distance Button"), tr("Simulate a distance button via the onroad UI to control personalities, 'Experimental Mode', and 'Traffic Mode'."), ""}, + + {"ExperimentalModeActivation", tr("Experimental Mode Activation"), tr("Toggle Experimental Mode with either buttons on the steering wheel or the screen. \n\nOverrides 'Conditional Experimental Mode'."), "../assets/img_experimental_white.svg"}, + {"ExperimentalModeViaLKAS", tr("Double Click LKAS"), tr("Enable/disable 'Experimental Mode' by double clicking the 'LKAS' button on your steering wheel."), ""}, + {"ExperimentalModeViaTap", tr("Double Tap the UI"), tr("Enable/disable 'Experimental Mode' by double tapping the onroad UI within a 0.5 second time frame."), ""}, + {"ExperimentalModeViaDistance", tr("Long Press Distance"), tr("Enable/disable 'Experimental Mode' by holding down the 'distance' button on your steering wheel for 0.5 seconds."), ""}, + + {"LaneChangeCustomizations", tr("Lane Change Customizations"), tr("Customize the lane change behaviors in openpilot."), "../frogpilot/assets/toggle_icons/icon_lane.png"}, + {"LaneChangeTime", tr("Lane Change Timer"), tr("Set a delay before executing a lane change."), ""}, + {"LaneDetectionWidth", tr("Lane Detection Threshold"), tr("Set the required lane width to be qualified as a lane."), ""}, + {"MinimumLaneChangeSpeed", tr("Minimum Lane Change Speed"), tr("Customize the minimum driving speed to allow openpilot to change lanes."), ""}, + {"NudgelessLaneChange", tr("Nudgeless Lane Change"), tr("Enable lane changes without requiring manual steering input."), ""}, + {"OneLaneChange", tr("One Lane Change Per Signal"), tr("Only allow one lane change per turn signal activation."), ""}, + + {"LateralTune", tr("Lateral Tuning"), tr("Modify openpilot's steering behavior."), "../frogpilot/assets/toggle_icons/icon_lateral_tune.png"}, + {"ForceAutoTune", tr("Force Auto Tune"), tr("Forces comma's auto lateral tuning for unsupported vehicles."), ""}, + {"NNFF", tr("NNFF"), tr("Use Twilsonco's Neural Network Feedforward for enhanced precision in lateral control."), ""}, + {"NNFFLite", tr("NNFF-Lite"), tr("Use Twilsonco's Neural Network Feedforward for enhanced precision in lateral control for cars without available NNFF logs."), ""}, + {"SteerRatio", steerRatioStock != 0 ? QString(tr("Steer Ratio (Default: %1)")).arg(QString::number(steerRatioStock, 'f', 2)) : tr("Steer Ratio"), tr("Use a custom steer ratio as opposed to comma's auto tune value."), ""}, + {"TacoTune", tr("Taco Tune"), tr("Use comma's 'Taco Tune' designed for handling left and right turns."), ""}, + {"TurnDesires", tr("Use Turn Desires"), tr("Use turn desires for greater precision in turns below the minimum lane change speed."), ""}, + + {"LongitudinalTune", tr("Longitudinal Tuning"), tr("Modify openpilot's acceleration and braking behavior."), "../frogpilot/assets/toggle_icons/icon_longitudinal_tune.png"}, + {"AccelerationProfile", tr("Acceleration Profile"), tr("Change the acceleration rate to be either sporty or eco-friendly."), ""}, + {"DecelerationProfile", tr("Deceleration Profile"), tr("Change the deceleration rate to be either sporty or eco-friendly."), ""}, + {"AggressiveAcceleration", tr("Increase Acceleration Behind Lead"), tr("Increase aggressiveness when following a faster lead."), ""}, + {"StoppingDistance", tr("Increase Stop Distance Behind Lead"), tr("Increase the stopping distance for a more comfortable stop from lead vehicles."), ""}, + {"LeadDetectionThreshold", tr("Lead Detection Threshold"), tr("Increase or decrease the lead detection threshold to either detect leads sooner, or increase model confidence."), ""}, + {"SmoothBraking", tr("Smoother Braking"), tr("Smoothen out the braking behavior when approaching slower vehicles."), ""}, + {"TrafficMode", tr("Traffic Mode"), tr("Enable the ability to activate 'Traffic Mode' by holding down the 'distance' button for 2.5 seconds. When 'Traffic Mode' is active the onroad UI will turn red and openpilot will drive catered towards stop and go traffic."), ""}, + + {"MTSCEnabled", tr("Map Turn Speed Control"), tr("Slow down for anticipated curves detected by the downloaded maps."), "../frogpilot/assets/toggle_icons/icon_speed_map.png"}, + {"DisableMTSCSmoothing", tr("Disable MTSC UI Smoothing"), tr("Disables the smoothing for the requested speed in the onroad UI to show exactly what speed MTSC is currently requesting."), ""}, + {"MTSCCurvatureCheck", tr("Model Curvature Detection Failsafe"), tr("Only trigger MTSC when the model detects a curve in the road. Purely used as a failsafe to prevent false positives. Leave this off if you never experience false positives."), ""}, + {"MTSCAggressiveness", tr("Turn Speed Aggressiveness"), tr("Set turn speed aggressiveness. Higher values result in faster turns, lower values yield gentler turns. \n\nA change of +- 1% results in the speed being raised or lowered by about 1 mph."), ""}, + + {"ModelSelector", tr("Model Selector"), tr("Manage openpilot's driving models."), "../assets/offroad/icon_calibration.png"}, + + {"QOLControls", tr("Quality of Life"), tr("Miscellaneous quality of life changes to improve your overall openpilot experience."), "../frogpilot/assets/toggle_icons/quality_of_life.png"}, + {"CustomCruise", tr("Cruise Increase Interval"), tr("Set a custom interval to increase the max set speed by."), ""}, + {"CustomCruiseLong", tr("Cruise Increase Interval (Long Press)"), tr("Set a custom interval to increase the max set speed by when holding down the cruise increase button."), ""}, + {"ForceStandstill", tr("Force Standstill State"), tr("Keeps openpilot in the 'standstill' state until the gas pedal is pressed."), ""}, + {"MapGears", tr("Map Accel/Decel To Gears"), tr("Map your acceleration/deceleration profile to your 'Eco' and/or 'Sport' gears."), ""}, + {"PauseLateralSpeed", tr("Pause Lateral Below"), tr("Pause lateral control on all speeds below the set speed."), ""}, + {"ReverseCruise", tr("Reverse Cruise Increase"), tr("Reverses the 'long press' functionality logic to increase the max set speed by 5 instead of 1. Useful to increase the max speed quickly."), ""}, + {"SetSpeedOffset", tr("Set Speed Offset"), tr("Set an offset for your desired set speed."), ""}, + + {"SpeedLimitController", tr("Speed Limit Controller"), tr("Automatically adjust the max speed to match the current speed limit using 'Open Street Maps', 'Navigate On openpilot', or your car's dashboard (Toyotas/Lexus/HKG only)."), "../assets/offroad/icon_speed_limit.png"}, + {"SLCControls", tr("Controls Settings"), tr("Manage toggles related to 'Speed Limit Controller's controls."), ""}, + {"Offset1", tr("Speed Limit Offset (0-34 mph)"), tr("Speed limit offset for speed limits between 0-34 mph."), ""}, + {"Offset2", tr("Speed Limit Offset (35-54 mph)"), tr("Speed limit offset for speed limits between 35-54 mph."), ""}, + {"Offset3", tr("Speed Limit Offset (55-64 mph)"), tr("Speed limit offset for speed limits between 55-64 mph."), ""}, + {"Offset4", tr("Speed Limit Offset (65-99 mph)"), tr("Speed limit offset for speed limits between 65-99 mph."), ""}, + {"SLCFallback", tr("Fallback Method"), tr("Choose your fallback method when there is no speed limit available."), ""}, + {"SLCOverride", tr("Override Method"), tr("Choose your preferred method to override the current speed limit."), ""}, + {"SLCPriority", tr("Priority Order"), tr("Configure the speed limit priority order."), ""}, + {"SLCQOL", tr("Quality of Life Settings"), tr("Manage toggles related to 'Speed Limit Controller's quality of life features."), ""}, + {"SLCConfirmation", tr("Confirm New Speed Limits"), tr("Don't automatically start using the new speed limit until it's been manually confirmed."), ""}, + {"ForceMPHDashboard", tr("Force MPH From Dashboard Readings"), tr("Force MPH readings from the dashboard. Only use this if you live in an area where the speed limits from your dashboard are in KPH, but you use MPH."), ""}, + {"SLCLookaheadHigher", tr("Prepare For Higher Speed Limits"), tr("Set a 'lookahead' value to prepare for upcoming speed limits higher than your current speed limit using the data stored in 'Open Street Maps'."), ""}, + {"SLCLookaheadLower", tr("Prepare For Lower Speed Limits"), tr("Set a 'lookahead' value to prepare for upcoming speed limits lower than your current speed limit using the data stored in 'Open Street Maps'."), ""}, + {"SetSpeedLimit", tr("Use Current Speed Limit As Set Speed"), tr("Sets your max speed to the current speed limit if one is populated when you initially enable openpilot."), ""}, + {"SLCVisuals", tr("Visuals Settings"), tr("Manage toggles related to 'Speed Limit Controller's visuals."), ""}, + {"ShowSLCOffset", tr("Show Speed Limit Offset"), tr("Show the speed limit offset separated from the speed limit in the onroad UI when using 'Speed Limit Controller'."), ""}, + {"SpeedLimitChangedAlert", tr("Speed Limit Changed Alert"), tr("Trigger an alert whenever the speed limit changes."), ""}, + {"UseVienna", tr("Use Vienna Speed Limit Signs"), tr("Use the Vienna (EU) speed limit style signs as opposed to MUTCD (US)."), ""}, + + {"VisionTurnControl", tr("Vision Turn Speed Controller"), tr("Slow down for detected curves in the road."), "../frogpilot/assets/toggle_icons/icon_vtc.png"}, + {"DisableVTSCSmoothing", tr("Disable VTSC UI Smoothing"), tr("Disables the smoothing for the requested speed in the onroad UI."), ""}, + {"CurveSensitivity", tr("Curve Detection Sensitivity"), tr("Set curve detection sensitivity. Higher values prompt earlier responses, lower values lead to smoother but later reactions."), ""}, + {"TurnAggressiveness", tr("Turn Speed Aggressiveness"), tr("Set turn speed aggressiveness. Higher values result in faster turns, lower values yield gentler turns."), ""}, + }; + + for (const auto &[param, title, desc, icon] : controlToggles) { + AbstractControl *controlToggle; + + if (param == "AlwaysOnLateral") { + FrogPilotParamManageControl *aolToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(aolToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(aolKeys.find(key.c_str()) != aolKeys.end()); + } + }); + controlToggle = aolToggle; + } else if (param == "PauseAOLOnBrake") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, std::map(), this, false, tr(" mph")); + + } else if (param == "ConditionalExperimental") { + FrogPilotParamManageControl *conditionalExperimentalToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(conditionalExperimentalToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + conditionalSpeedsImperial->setVisible(!isMetric); + conditionalSpeedsMetric->setVisible(isMetric); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(conditionalExperimentalKeys.find(key.c_str()) != conditionalExperimentalKeys.end()); + } + }); + controlToggle = conditionalExperimentalToggle; + } else if (param == "CECurves") { + FrogPilotParamValueControl *CESpeedImperial = new FrogPilotParamValueControl("CESpeed", tr("Below"), tr("Switch to 'Experimental Mode' below this speed when not following a lead vehicle."), "", 0, 99, + std::map(), this, false, tr(" mph")); + FrogPilotParamValueControl *CESpeedLeadImperial = new FrogPilotParamValueControl("CESpeedLead", tr(" w/Lead"), tr("Switch to 'Experimental Mode' below this speed when following a lead vehicle."), "", 0, 99, + std::map(), this, false, tr(" mph")); + conditionalSpeedsImperial = new FrogPilotDualParamControl(CESpeedImperial, CESpeedLeadImperial, this); + addItem(conditionalSpeedsImperial); + + FrogPilotParamValueControl *CESpeedMetric = new FrogPilotParamValueControl("CESpeed", tr("Below"), tr("Switch to 'Experimental Mode' below this speed in absence of a lead vehicle."), "", 0, 150, + std::map(), this, false, tr(" kph")); + FrogPilotParamValueControl *CESpeedLeadMetric = new FrogPilotParamValueControl("CESpeedLead", tr(" w/Lead"), tr("Switch to 'Experimental Mode' below this speed when following a lead vehicle."), "", 0, 150, + std::map(), this, false, tr(" kph")); + conditionalSpeedsMetric = new FrogPilotDualParamControl(CESpeedMetric, CESpeedLeadMetric, this); + addItem(conditionalSpeedsMetric); + + std::vector curveToggles{"CECurvesLead"}; + std::vector curveToggleNames{tr("With Lead")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, curveToggles, curveToggleNames); + } else if (param == "CELead") { + std::vector leadToggles{"CESlowerLead", "CEStoppedLead"}; + std::vector leadToggleNames{tr("Slower Lead"), tr("Stopped Lead")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, leadToggles, leadToggleNames); + } else if (param == "CENavigation") { + std::vector navigationToggles{"CENavigationIntersections", "CENavigationTurns", "CENavigationLead"}; + std::vector navigationToggleNames{tr("Intersections"), tr("Turns"), tr("With Lead")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, navigationToggles, navigationToggleNames); + } else if (param == "CEStopLights") { + std::vector stopLightsToggles{"CEStopLightsLessSensitive"}; + std::vector stopLightsToggleNames{tr("Decrease Sensitivity")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, stopLightsToggles, stopLightsToggleNames); + + } else if (param == "DeviceManagement") { + FrogPilotParamManageControl *deviceManagementToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(deviceManagementToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(deviceManagementKeys.find(key.c_str()) != deviceManagementKeys.end()); + } + }); + controlToggle = deviceManagementToggle; + } else if (param == "DeviceShutdown") { + std::map shutdownLabels; + for (int i = 0; i <= 33; ++i) { + shutdownLabels[i] = i == 0 ? tr("5 mins") : i <= 3 ? QString::number(i * 15) + tr(" mins") : QString::number(i - 3) + (i == 4 ? tr(" hour") : tr(" hours")); + } + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 33, shutdownLabels, this, false); + } else if (param == "NoUploads") { + std::vector uploadsToggles{"DisableOnroadUploads"}; + std::vector uploadsToggleNames{tr("Only Onroad")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, uploadsToggles, uploadsToggleNames); + } else if (param == "LowVoltageShutdown") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 11.8, 12.5, std::map(), this, false, tr(" volts"), 1, 0.01); + + } else if (param == "DrivingPersonalities") { + FrogPilotParamManageControl *drivingPersonalitiesToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(drivingPersonalitiesToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(drivingPersonalityKeys.find(key.c_str()) != drivingPersonalityKeys.end()); + } + }); + controlToggle = drivingPersonalitiesToggle; + } else if (param == "CustomPersonalities") { + FrogPilotParamManageControl *customPersonalitiesToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(customPersonalitiesToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + customPersonalitiesOpen = true; + for (auto &[key, toggle] : toggles) { + toggle->setVisible(customdrivingPersonalityKeys.find(key.c_str()) != customdrivingPersonalityKeys.end()); + openSubParentToggle(); + } + }); + + personalitiesInfoBtn = new ButtonControl(tr("What Do All These Do?"), tr("VIEW"), tr("Learn what all the values in 'Custom Personality Profiles' do on openpilot's driving behaviors.")); + connect(personalitiesInfoBtn, &ButtonControl::clicked, [=]() { + const std::string txt = util::read_file("../frogpilot/ui/qt/offroad/personalities_info.txt"); + ConfirmationDialog::rich(QString::fromStdString(txt), this); + }); + addItem(personalitiesInfoBtn); + + controlToggle = customPersonalitiesToggle; + } else if (param == "ResetTrafficPersonality" || param == "ResetAggressivePersonality" || param == "ResetStandardPersonality" || param == "ResetRelaxedPersonality") { + std::vector personalityOptions{tr("Reset")}; + FrogPilotButtonsControl *profileBtn = new FrogPilotButtonsControl(title, desc, icon, personalityOptions); + controlToggle = profileBtn; + } else if (param == "TrafficPersonalityProfile") { + FrogPilotParamManageControl *trafficPersonalityToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(trafficPersonalityToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + for (auto &[key, toggle] : toggles) { + toggle->setVisible(trafficPersonalityKeys.find(key.c_str()) != trafficPersonalityKeys.end()); + } + openSubSubParentToggle(); + personalitiesInfoBtn->setVisible(true); + }); + controlToggle = trafficPersonalityToggle; + } else if (param == "AggressivePersonalityProfile") { + FrogPilotParamManageControl *aggressivePersonalityToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(aggressivePersonalityToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + for (auto &[key, toggle] : toggles) { + toggle->setVisible(aggressivePersonalityKeys.find(key.c_str()) != aggressivePersonalityKeys.end()); + } + openSubSubParentToggle(); + personalitiesInfoBtn->setVisible(true); + }); + controlToggle = aggressivePersonalityToggle; + } else if (param == "StandardPersonalityProfile") { + FrogPilotParamManageControl *standardPersonalityToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(standardPersonalityToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + for (auto &[key, toggle] : toggles) { + toggle->setVisible(standardPersonalityKeys.find(key.c_str()) != standardPersonalityKeys.end()); + } + openSubSubParentToggle(); + personalitiesInfoBtn->setVisible(true); + }); + controlToggle = standardPersonalityToggle; + } else if (param == "RelaxedPersonalityProfile") { + FrogPilotParamManageControl *relaxedPersonalityToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(relaxedPersonalityToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + for (auto &[key, toggle] : toggles) { + toggle->setVisible(relaxedPersonalityKeys.find(key.c_str()) != relaxedPersonalityKeys.end()); + } + openSubSubParentToggle(); + personalitiesInfoBtn->setVisible(true); + }); + controlToggle = relaxedPersonalityToggle; + } else if (trafficPersonalityKeys.find(param) != trafficPersonalityKeys.end() || + aggressivePersonalityKeys.find(param) != aggressivePersonalityKeys.end() || + standardPersonalityKeys.find(param) != standardPersonalityKeys.end() || + relaxedPersonalityKeys.find(param) != relaxedPersonalityKeys.end()) { + if (param == "TrafficFollow" || param == "AggressiveFollow" || param == "StandardFollow" || param == "RelaxedFollow") { + if (param == "TrafficFollow") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0.5, 5, std::map(), this, false, tr(" seconds"), 1, 0.01); + } else { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 5, std::map(), this, false, tr(" seconds"), 1, 0.01); + } + } else { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 500, std::map(), this, false, "%"); + } + } else if (param == "OnroadDistanceButton") { + std::vector onroadDistanceToggles{"KaofuiIcons"}; + std::vector onroadDistanceToggleNames{tr("Kaofui's Icons")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, onroadDistanceToggles, onroadDistanceToggleNames); + + } else if (param == "ExperimentalModeActivation") { + FrogPilotParamManageControl *experimentalModeActivationToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(experimentalModeActivationToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedExperimentalModeActivationKeys = experimentalModeActivationKeys; + + if (params.getBool("AlwaysOnLateralLKAS")) { + modifiedExperimentalModeActivationKeys.erase("ExperimentalModeViaLKAS"); + } + + toggle->setVisible(modifiedExperimentalModeActivationKeys.find(key.c_str()) != modifiedExperimentalModeActivationKeys.end()); + } + }); + controlToggle = experimentalModeActivationToggle; + + } else if (param == "LateralTune") { + FrogPilotParamManageControl *lateralTuneToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(lateralTuneToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedLateralTuneKeys = lateralTuneKeys; + + if (hasAutoTune || params.getBool("LateralTune") && params.getBool("NNFF")) { + modifiedLateralTuneKeys.erase("ForceAutoTune"); + } + + if (hasCommaNNFFSupport || !hasNNFFLog) { + modifiedLateralTuneKeys.erase("NNFF"); + } else { + modifiedLateralTuneKeys.erase("NNFFLite"); + } + + toggle->setVisible(modifiedLateralTuneKeys.find(key.c_str()) != modifiedLateralTuneKeys.end()); + } + }); + controlToggle = lateralTuneToggle; + } else if (param == "SteerRatio") { + std::vector steerRatioToggles{"ResetSteerRatio"}; + std::vector steerRatioToggleNames{"Reset"}; + controlToggle = new FrogPilotParamValueToggleControl(param, title, desc, icon, steerRatioStock * 0.75, steerRatioStock * 1.25, std::map(), this, false, "", 1, 0.01, steerRatioToggles, steerRatioToggleNames); + + } else if (param == "LongitudinalTune") { + FrogPilotParamManageControl *longitudinalTuneToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(longitudinalTuneToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedLongitudinalTuneKeys = longitudinalTuneKeys; + + if (params.get("Model") == "radical-turtle") { + modifiedLongitudinalTuneKeys.erase("LeadDetectionThreshold"); + } + + toggle->setVisible(modifiedLongitudinalTuneKeys.find(key.c_str()) != modifiedLongitudinalTuneKeys.end()); + } + }); + controlToggle = longitudinalTuneToggle; + } else if (param == "AccelerationProfile") { + std::vector profileOptions{tr("Standard"), tr("Eco"), tr("Sport"), tr("Sport+")}; + FrogPilotButtonParamControl *profileSelection = new FrogPilotButtonParamControl(param, title, desc, icon, profileOptions); + controlToggle = profileSelection; + } else if (param == "DecelerationProfile") { + std::vector profileOptions{tr("Standard"), tr("Eco"), tr("Sport")}; + FrogPilotButtonParamControl *profileSelection = new FrogPilotButtonParamControl(param, title, desc, icon, profileOptions); + controlToggle = profileSelection; + } else if (param == "StoppingDistance") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 10, std::map(), this, false, tr(" feet")); + } else if (param == "LeadDetectionThreshold") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 99, std::map(), this, false, "%"); + + } else if (param == "MTSCEnabled") { + FrogPilotParamManageControl *mtscToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(mtscToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(mtscKeys.find(key.c_str()) != mtscKeys.end()); + } + }); + controlToggle = mtscToggle; + } else if (param == "MTSCAggressiveness") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 200, std::map(), this, false, "%"); + + } else if (param == "ModelSelector") { + FrogPilotParamManageControl *modelsToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(modelsToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(false); + } + + deleteModelBtn->setVisible(true); + downloadModelBtn->setVisible(true); + selectModelBtn->setVisible(true); + }); + controlToggle = modelsToggle; + + QDir modelDir("/data/models/"); + + deleteModelBtn = new ButtonControl(tr("Delete Model"), tr("DELETE"), ""); + QObject::connect(deleteModelBtn, &ButtonControl::clicked, [=]() { + std::string currentModel = params.get("Model") + ".thneed"; + + QStringList availableModels = QString::fromStdString(params.get("AvailableModels")).split(","); + QStringList modelLabels = QString::fromStdString(params.get("AvailableModelsNames")).split(","); + + QStringList existingModelFiles = modelDir.entryList({"*.thneed"}, QDir::Files); + QMap labelToFileMap; + QStringList deletableModelLabels; + for (int i = 0; i < availableModels.size(); ++i) { + QString modelFileName = availableModels[i] + ".thneed"; + if (existingModelFiles.contains(modelFileName) && modelFileName != QString::fromStdString(currentModel)) { + QString readableName = modelLabels[i]; + deletableModelLabels.append(readableName); + labelToFileMap[readableName] = modelFileName; + } + } + + QString selectedModel = MultiOptionDialog::getSelection(tr("Select a model to delete"), deletableModelLabels, "", this); + if (!selectedModel.isEmpty() && ConfirmationDialog::confirm(tr("Are you sure you want to delete this model?"), tr("Delete"), this)) { + std::thread([=]() { + deleteModelBtn->setValue(tr("Deleting...")); + + deleteModelBtn->setEnabled(false); + downloadModelBtn->setEnabled(false); + selectModelBtn->setEnabled(false); + + QString modelToDelete = labelToFileMap[selectedModel]; + + QFile::remove(modelDir.absoluteFilePath(modelToDelete)); + + deleteModelBtn->setEnabled(true); + downloadModelBtn->setEnabled(true); + selectModelBtn->setEnabled(true); + + deleteModelBtn->setValue(tr("Deleted!")); + std::this_thread::sleep_for(std::chrono::seconds(3)); + deleteModelBtn->setValue(""); + }).detach(); + } + }); + addItem(deleteModelBtn); + + downloadModelBtn = new ButtonControl(tr("Download Model"), tr("DOWNLOAD"), ""); + QObject::connect(downloadModelBtn, &ButtonControl::clicked, [=]() { + QStringList availableModels = QString::fromStdString(params.get("AvailableModels")).split(","); + QStringList modelLabels = QString::fromStdString(params.get("AvailableModelsNames")).split(","); + + QMap labelToModelMap; + QStringList downloadableModelLabels; + QStringList existingModelFiles = modelDir.entryList({"*.thneed"}, QDir::Files); + for (int i = 0; i < availableModels.size(); ++i) { + QString modelFileName = availableModels.at(i) + ".thneed"; + if (!existingModelFiles.contains(modelFileName)) { + QString readableName = modelLabels.at(i); + if (!readableName.contains("(Default)")) { + downloadableModelLabels.append(readableName); + labelToModelMap.insert(readableName, availableModels.at(i)); + } + } + } + + QString modelToDownload = MultiOptionDialog::getSelection(tr("Select a driving model to download"), downloadableModelLabels, "", this); + if (!modelToDownload.isEmpty()) { + QString selectedModelValue = labelToModelMap.value(modelToDownload); + paramsMemory.put("ModelToDownload", selectedModelValue.toStdString()); + + deleteModelBtn->setEnabled(false); + downloadModelBtn->setEnabled(false); + selectModelBtn->setEnabled(false); + + QTimer *failureTimer = new QTimer(this); + failureTimer->setSingleShot(true); + + QTimer *progressTimer = new QTimer(this); + progressTimer->setInterval(100); + + connect(failureTimer, &QTimer::timeout, this, [=]() { + deleteModelBtn->setEnabled(true); + downloadModelBtn->setEnabled(true); + selectModelBtn->setEnabled(true); + + downloadModelBtn->setValue(tr("Download failed...")); + paramsMemory.remove("ModelDownloadProgress"); + paramsMemory.remove("ModelToDownload"); + + progressTimer->stop(); + progressTimer->deleteLater(); + + QTimer::singleShot(3000, this, [this]() { + downloadModelBtn->setValue(""); + }); + }); + + connect(progressTimer, &QTimer::timeout, this, [=]() { + static int lastProgress = -1; + int progress = paramsMemory.getInt("ModelDownloadProgress"); + + if (progress == lastProgress) { + if (!failureTimer->isActive()) { + failureTimer->start(30000); + } + } else { + lastProgress = progress; + downloadModelBtn->setValue(QString::number(progress) + "%"); + failureTimer->stop(); + + if (progress == 100) { + deleteModelBtn->setEnabled(true); + downloadModelBtn->setEnabled(true); + selectModelBtn->setEnabled(true); + + downloadModelBtn->setValue(tr("Downloaded!")); + paramsMemory.remove("ModelDownloadProgress"); + paramsMemory.remove("ModelToDownload"); + + progressTimer->stop(); + progressTimer->deleteLater(); + + QTimer::singleShot(3000, this, [this]() { + if (paramsMemory.get("ModelDownloadProgress").empty()) { + downloadModelBtn->setValue(""); + } + }); + } + } + }); + progressTimer->start(); + } + }); + addItem(downloadModelBtn); + + selectModelBtn = new ButtonControl(tr("Select Model"), tr("SELECT"), ""); + QObject::connect(selectModelBtn, &ButtonControl::clicked, [=]() { + QStringList availableModels = QString::fromStdString(params.get("AvailableModels")).split(","); + QStringList modelLabels = QString::fromStdString(params.get("AvailableModelsNames")).split(","); + + QStringList modelFiles = modelDir.entryList({"*.thneed"}, QDir::Files); + QSet modelFilesBaseNames; + for (const QString &modelFile : modelFiles) { + modelFilesBaseNames.insert(modelFile.section('.', 0, 0)); + } + + QStringList selectableModelLabels; + for (int i = 0; i < availableModels.size(); ++i) { + if (modelFilesBaseNames.contains(availableModels[i]) || modelLabels[i].contains("(Default)")) { + selectableModelLabels.append(modelLabels[i]); + } + } + + QString modelToSelect = MultiOptionDialog::getSelection(tr("Select a model - 🗺️ = Navigation | 📡 = Radar | 👀 = VOACC"), selectableModelLabels, "", this); + if (!modelToSelect.isEmpty()) { + selectModelBtn->setValue(modelToSelect); + + int modelIndex = modelLabels.indexOf(modelToSelect); + if (modelIndex != -1) { + QString selectedModel = availableModels.at(modelIndex); + params.putNonBlocking("Model", selectedModel.toStdString()); + params.putNonBlocking("ModelName", modelToSelect.toStdString()); + } + + if (FrogPilotConfirmationDialog::yesorno(tr("Do you want to start with a fresh calibration for the newly selected model?"), this)) { + params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); + } + + if (started) { + if (FrogPilotConfirmationDialog::toggle(tr("Reboot required to take effect."), tr("Reboot Now"), this)) { + Hardware::reboot(); + } + } + } + }); + addItem(selectModelBtn); + selectModelBtn->setValue(QString::fromStdString(params.get("ModelName"))); + + } else if (param == "QOLControls") { + FrogPilotParamManageControl *qolToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(qolToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedQolKeys = qolKeys; + + if (!hasPCMCruise) { + modifiedQolKeys.erase("ReverseCruise"); + } else { + modifiedQolKeys.erase("CustomCruise"); + modifiedQolKeys.erase("CustomCruiseLong"); + modifiedQolKeys.erase("SetSpeedOffset"); + } + + if (!isToyota && !isGM && !isHKGCanFd) { + modifiedQolKeys.erase("MapGears"); + } + + toggle->setVisible(modifiedQolKeys.find(key.c_str()) != modifiedQolKeys.end()); + } + }); + controlToggle = qolToggle; + } else if (param == "CustomCruise") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 99, std::map(), this, false, tr(" mph")); + } else if (param == "CustomCruiseLong") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 99, std::map(), this, false, tr(" mph")); + } else if (param == "ForceStandstill") { + std::vector forceStopToggles{"ForceStops"}; + std::vector forceStopToggleNames{tr("Only For Stop Lights/Stop Signs")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, forceStopToggles, forceStopToggleNames); + } else if (param == "MapGears") { + std::vector mapGearsToggles{"MapAcceleration", "MapDeceleration"}; + std::vector mapGearsToggleNames{tr("Acceleration"), tr("Deceleration")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, mapGearsToggles, mapGearsToggleNames); + } else if (param == "PauseLateralSpeed") { + std::vector pauseLateralToggles{"PauseLateralOnSignal"}; + std::vector pauseLateralToggleNames{"Turn Signal Only"}; + controlToggle = new FrogPilotParamValueToggleControl(param, title, desc, icon, 0, 99, std::map(), this, false, tr(" mph"), 1, 1, pauseLateralToggles, pauseLateralToggleNames); + } else if (param == "PauseLateralOnSignal") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, std::map(), this, false, tr(" mph")); + } else if (param == "ReverseCruise") { + std::vector reverseCruiseToggles{"ReverseCruiseUI"}; + std::vector reverseCruiseNames{tr("Control Via UI")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, reverseCruiseToggles, reverseCruiseNames); + } else if (param == "SetSpeedOffset") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, std::map(), this, false, tr(" mph")); + + } else if (param == "LaneChangeCustomizations") { + FrogPilotParamManageControl *laneChangeToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(laneChangeToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(laneChangeKeys.find(key.c_str()) != laneChangeKeys.end()); + } + }); + controlToggle = laneChangeToggle; + } else if (param == "LaneChangeTime") { + std::map laneChangeTimeLabels; + for (int i = 0; i <= 10; ++i) { + laneChangeTimeLabels[i] = i == 0 ? "Instant" : QString::number(i / 2.0) + " seconds"; + } + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 10, laneChangeTimeLabels, this, false); + } else if (param == "LaneDetectionWidth") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 100, std::map(), this, false, " feet", 10); + } else if (param == "MinimumLaneChangeSpeed") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, std::map(), this, false, tr(" mph")); + + } else if (param == "SpeedLimitController") { + FrogPilotParamManageControl *speedLimitControllerToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(speedLimitControllerToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + slcOpen = true; + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(speedLimitControllerKeys.find(key.c_str()) != speedLimitControllerKeys.end()); + } + }); + controlToggle = speedLimitControllerToggle; + } else if (param == "SLCControls") { + FrogPilotParamManageControl *manageSLCControlsToggle = new FrogPilotParamManageControl(param, title, desc, icon, this, true); + QObject::connect(manageSLCControlsToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + for (auto &[key, toggle] : toggles) { + toggle->setVisible(speedLimitControllerControlsKeys.find(key.c_str()) != speedLimitControllerControlsKeys.end()); + openSubParentToggle(); + } + }); + controlToggle = manageSLCControlsToggle; + } else if (param == "SLCQOL") { + FrogPilotParamManageControl *manageSLCQOLToggle = new FrogPilotParamManageControl(param, title, desc, icon, this, true); + QObject::connect(manageSLCQOLToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + for (auto &[key, toggle] : toggles) { + std::set modifiedSpeedLimitControllerQOLKeys = speedLimitControllerQOLKeys; + + if (hasPCMCruise) { + modifiedSpeedLimitControllerQOLKeys.erase("SetSpeedLimit"); + } + + if (!isToyota) { + modifiedSpeedLimitControllerQOLKeys.erase("ForceMPHDashboard"); + } + + toggle->setVisible(modifiedSpeedLimitControllerQOLKeys.find(key.c_str()) != modifiedSpeedLimitControllerQOLKeys.end()); + openSubParentToggle(); + } + }); + controlToggle = manageSLCQOLToggle; + } else if (param == "SLCConfirmation") { + std::vector slcConfirmationToggles{"SLCConfirmationLower", "SLCConfirmationHigher"}; + std::vector slcConfirmationNames{tr("Lower Limits"), tr("Higher Limits")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, slcConfirmationToggles, slcConfirmationNames); + } else if (param == "SLCLookaheadHigher" || param == "SLCLookaheadLower") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 60, std::map(), this, false, " seconds"); + } else if (param == "SLCVisuals") { + FrogPilotParamManageControl *manageSLCVisualsToggle = new FrogPilotParamManageControl(param, title, desc, icon, this, true); + QObject::connect(manageSLCVisualsToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + for (auto &[key, toggle] : toggles) { + toggle->setVisible(speedLimitControllerVisualsKeys.find(key.c_str()) != speedLimitControllerVisualsKeys.end()); + openSubParentToggle(); + } + }); + controlToggle = manageSLCVisualsToggle; + } else if (param == "Offset1" || param == "Offset2" || param == "Offset3" || param == "Offset4") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, -99, 99, std::map(), this, false, tr(" mph")); + } else if (param == "ShowSLCOffset") { + std::vector slcOffsetToggles{"ShowSLCOffsetUI"}; + std::vector slcOffsetToggleNames{tr("Control Via UI")}; + controlToggle = new FrogPilotParamToggleControl(param, title, desc, icon, slcOffsetToggles, slcOffsetToggleNames); + } else if (param == "SLCFallback") { + std::vector fallbackOptions{tr("Set Speed"), tr("Experimental Mode"), tr("Previous Limit")}; + FrogPilotButtonParamControl *fallbackSelection = new FrogPilotButtonParamControl(param, title, desc, icon, fallbackOptions); + controlToggle = fallbackSelection; + } else if (param == "SLCOverride") { + std::vector overrideOptions{tr("None"), tr("Manual Set Speed"), tr("Set Speed")}; + FrogPilotButtonParamControl *overrideSelection = new FrogPilotButtonParamControl(param, title, desc, icon, overrideOptions); + controlToggle = overrideSelection; + } else if (param == "SLCPriority") { + ButtonControl *slcPriorityButton = new ButtonControl(title, tr("SELECT"), desc); + QStringList primaryPriorities = {tr("None"), tr("Dashboard"), tr("Navigation"), tr("Offline Maps"), tr("Highest"), tr("Lowest")}; + QStringList secondaryTertiaryPriorities = {tr("None"), tr("Dashboard"), tr("Navigation"), tr("Offline Maps")}; + QStringList priorityPrompts = {tr("Select your primary priority"), tr("Select your secondary priority"), tr("Select your tertiary priority")}; + + QObject::connect(slcPriorityButton, &ButtonControl::clicked, [=]() { + QStringList selectedPriorities; + + for (int i = 1; i <= 3; ++i) { + QStringList currentPriorities = (i == 1) ? primaryPriorities : secondaryTertiaryPriorities; + QStringList prioritiesToDisplay = currentPriorities; + for (const auto &selectedPriority : qAsConst(selectedPriorities)) { + prioritiesToDisplay.removeAll(selectedPriority); + } + + if (!hasDashSpeedLimits) { + prioritiesToDisplay.removeAll(tr("Dashboard")); + } + + if (prioritiesToDisplay.size() == 1 && prioritiesToDisplay.contains(tr("None"))) { + break; + } + + QString priorityKey = QString("SLCPriority%1").arg(i); + QString selection = MultiOptionDialog::getSelection(priorityPrompts[i - 1], prioritiesToDisplay, "", this); + + if (selection.isEmpty()) break; + + params.putNonBlocking(priorityKey.toStdString(), selection.toStdString()); + selectedPriorities.append(selection); + + if (selection == tr("Lowest") || selection == tr("Highest") || selection == tr("None")) break; + + updateFrogPilotToggles(); + } + + selectedPriorities.removeAll(tr("None")); + slcPriorityButton->setValue(selectedPriorities.join(", ")); + }); + + QStringList initialPriorities; + for (int i = 1; i <= 3; ++i) { + QString priorityKey = QString("SLCPriority%1").arg(i); + QString priority = QString::fromStdString(params.get(priorityKey.toStdString())); + + if (!priority.isEmpty() && primaryPriorities.contains(priority) && priority != tr("None")) { + initialPriorities.append(priority); + } + } + slcPriorityButton->setValue(initialPriorities.join(", ")); + controlToggle = slcPriorityButton; + + } else if (param == "VisionTurnControl") { + FrogPilotParamManageControl *visionTurnControlToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(visionTurnControlToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(visionTurnControlKeys.find(key.c_str()) != visionTurnControlKeys.end()); + } + }); + controlToggle = visionTurnControlToggle; + } else if (param == "CurveSensitivity" || param == "TurnAggressiveness") { + controlToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 200, std::map(), this, false, "%"); + + } else { + controlToggle = new ParamControl(param, title, desc, icon, this); + } + + addItem(controlToggle); + toggles[param.toStdString()] = controlToggle; + + QObject::connect(static_cast(controlToggle), &ToggleControl::toggleFlipped, &updateFrogPilotToggles); + QObject::connect(static_cast(controlToggle), &FrogPilotParamToggleControl::buttonTypeClicked, &updateFrogPilotToggles); + QObject::connect(static_cast(controlToggle), &FrogPilotParamValueControl::valueChanged, &updateFrogPilotToggles); + + ParamWatcher *param_watcher = new ParamWatcher(this); + param_watcher->addParam("CESpeed"); + param_watcher->addParam("CESpeedLead"); + + QObject::connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + updateFrogPilotToggles(); + }); + + QObject::connect(controlToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + + QObject::connect(static_cast(controlToggle), &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + update(); + }); + } + + QObject::connect(static_cast(toggles["IncreaseThermalLimits"]), &ToggleControl::toggleFlipped, [this](bool state) { + if (state) { + FrogPilotConfirmationDialog::toggleAlert( + tr("WARNING: This can cause premature wear or damage by running the device over comma's recommended temperature limits!"), + tr("I understand the risks."), this); + } + }); + + QObject::connect(static_cast(toggles["NoLogging"]), &ToggleControl::toggleFlipped, [this](bool state) { + if (state) { + FrogPilotConfirmationDialog::toggleAlert( + tr("WARNING: This will prevent your drives from being recorded and the data will be unobtainable!"), + tr("I understand the risks."), this); + } + }); + + QObject::connect(static_cast(toggles["NoUploads"]), &ToggleControl::toggleFlipped, [this](bool state) { + if (state) { + FrogPilotConfirmationDialog::toggleAlert( + tr("WARNING: This will prevent your drives from appearing on comma connect which may impact debugging and support!"), + tr("I understand the risks."), this); + } + }); + + QObject::connect(static_cast(toggles["TrafficMode"]), &ToggleControl::toggleFlipped, [this](bool state) { + if (state) { + FrogPilotConfirmationDialog::toggleAlert( + tr("To activate 'Traffic Mode' you hold down the 'distance' button on your steering wheel for 2.5 seconds."), + tr("Sounds good!"), this); + } + }); + + std::set rebootKeys = {"AlwaysOnLateral", "NNFF", "NNFFLite"}; + for (const QString &key : rebootKeys) { + QObject::connect(static_cast(toggles[key.toStdString().c_str()]), &ToggleControl::toggleFlipped, [this, key](bool state) { + if (started) { + if (key == "AlwaysOnLateral" && state) { + if (FrogPilotConfirmationDialog::toggle(tr("Reboot required to take effect."), tr("Reboot Now"), this)) { + Hardware::reboot(); + } + } else if (key != "AlwaysOnLateral") { + if (FrogPilotConfirmationDialog::toggle(tr("Reboot required to take effect."), tr("Reboot Now"), this)) { + Hardware::reboot(); + } + } + } + }); + } + + FrogPilotParamValueControl *trafficFollowToggle = static_cast(toggles["TrafficFollow"]); + FrogPilotParamValueControl *trafficAccelerationToggle = static_cast(toggles["TrafficJerkAcceleration"]); + FrogPilotParamValueControl *trafficDangerToggle = static_cast(toggles["TrafficJerkDanger"]); + FrogPilotParamValueControl *trafficSpeedToggle = static_cast(toggles["TrafficJerkSpeed"]); + FrogPilotButtonsControl *trafficResetButton = static_cast(toggles["ResetTrafficPersonality"]); + + QObject::connect(trafficResetButton, &FrogPilotButtonsControl::buttonClicked, this, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for the 'Traffic Mode' personality?"), this)) { + params.putFloat("TrafficFollow", 0.5); + params.putFloat("TrafficJerkAcceleration", 50); + params.putFloat("TrafficJerkDanger", 100); + params.putFloat("TrafficJerkSpeed", 50); + trafficFollowToggle->refresh(); + trafficAccelerationToggle->refresh(); + trafficDangerToggle->refresh(); + trafficSpeedToggle->refresh(); + updateFrogPilotToggles(); + } + }); + + FrogPilotParamValueControl *aggressiveFollowToggle = static_cast(toggles["AggressiveFollow"]); + FrogPilotParamValueControl *aggressiveAccelerationToggle = static_cast(toggles["AggressiveJerkAcceleration"]); + FrogPilotParamValueControl *aggressiveDangerToggle = static_cast(toggles["AggressiveJerkDanger"]); + FrogPilotParamValueControl *aggressiveSpeedToggle = static_cast(toggles["AggressiveJerkSpeed"]); + FrogPilotButtonsControl *aggressiveResetButton = static_cast(toggles["ResetAggressivePersonality"]); + + QObject::connect(aggressiveResetButton, &FrogPilotButtonsControl::buttonClicked, this, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for the 'Aggressive' personality?"), this)) { + params.putFloat("AggressiveFollow", 1.25); + params.putFloat("AggressiveJerkAcceleration", 50); + params.putFloat("AggressiveJerkDanger", 100); + params.putFloat("AggressiveJerkSpeed", 50); + aggressiveFollowToggle->refresh(); + aggressiveAccelerationToggle->refresh(); + aggressiveDangerToggle->refresh(); + aggressiveSpeedToggle->refresh(); + updateFrogPilotToggles(); + } + }); + + FrogPilotParamValueControl *standardFollowToggle = static_cast(toggles["StandardFollow"]); + FrogPilotParamValueControl *standardAccelerationToggle = static_cast(toggles["StandardJerkAcceleration"]); + FrogPilotParamValueControl *standardDangerToggle = static_cast(toggles["StandardJerkDanger"]); + FrogPilotParamValueControl *standardSpeedToggle = static_cast(toggles["StandardJerkSpeed"]); + FrogPilotButtonsControl *standardResetButton = static_cast(toggles["ResetStandardPersonality"]); + + QObject::connect(standardResetButton, &FrogPilotButtonsControl::buttonClicked, this, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for the 'Standard' personality?"), this)) { + params.putFloat("StandardFollow", 1.45); + params.putFloat("StandardJerkAcceleration", 100); + params.putFloat("StandardJerkDanger", 100); + params.putFloat("StandardJerkSpeed", 100); + standardFollowToggle->refresh(); + standardAccelerationToggle->refresh(); + standardDangerToggle->refresh(); + standardSpeedToggle->refresh(); + updateFrogPilotToggles(); + } + }); + + FrogPilotParamValueControl *relaxedFollowToggle = static_cast(toggles["RelaxedFollow"]); + FrogPilotParamValueControl *relaxedAccelerationToggle = static_cast(toggles["RelaxedJerkAcceleration"]); + FrogPilotParamValueControl *relaxedDangerToggle = static_cast(toggles["RelaxedJerkDanger"]); + FrogPilotParamValueControl *relaxedSpeedToggle = static_cast(toggles["RelaxedJerkSpeed"]); + FrogPilotButtonsControl *relaxedResetButton = static_cast(toggles["ResetRelaxedPersonality"]); + + QObject::connect(relaxedResetButton, &FrogPilotButtonsControl::buttonClicked, this, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for the 'Relaxed' personality?"), this)) { + params.putFloat("RelaxedFollow", 1.75); + params.putFloat("RelaxedJerkAcceleration", 100); + params.putFloat("RelaxedJerkDanger", 100); + params.putFloat("RelaxedJerkSpeed", 100); + relaxedFollowToggle->refresh(); + relaxedAccelerationToggle->refresh(); + relaxedDangerToggle->refresh(); + relaxedSpeedToggle->refresh(); + updateFrogPilotToggles(); + } + }); + + modelManagerToggle = static_cast(toggles["ModelSelector"]); + steerRatioToggle = static_cast(toggles["SteerRatio"]); + + QObject::connect(steerRatioToggle, &FrogPilotParamValueToggleControl::buttonClicked, this, [this]() { + params.putFloat("SteerRatio", steerRatioStock); + params.putBool("ResetSteerRatio", false); + steerRatioToggle->refresh(); + updateFrogPilotToggles(); + }); + + QObject::connect(parent, &SettingsWindow::closeParentToggle, this, &FrogPilotControlsPanel::hideToggles); + QObject::connect(parent, &SettingsWindow::closeSubParentToggle, this, &FrogPilotControlsPanel::hideSubToggles); + QObject::connect(parent, &SettingsWindow::closeSubSubParentToggle, this, &FrogPilotControlsPanel::hideSubSubToggles); + QObject::connect(parent, &SettingsWindow::updateMetric, this, &FrogPilotControlsPanel::updateMetric); + QObject::connect(uiState(), &UIState::offroadTransition, this, &FrogPilotControlsPanel::updateCarToggles); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotControlsPanel::updateState); + + updateMetric(); +} + +void FrogPilotControlsPanel::showEvent(QShowEvent *event, const UIState &s) { + hasOpenpilotLongitudinal &= !params.getBool("DisableOpenpilotLongitudinal"); + + downloadModelBtn->setEnabled(s.scene.online); +} + +void FrogPilotControlsPanel::updateState(const UIState &s) { + if (!isVisible()) return; + + started = s.scene.started; +} + +void FrogPilotControlsPanel::updateCarToggles() { + auto carParams = params.get("CarParamsPersistent"); + if (!carParams.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(carParams.data(), carParams.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + auto carFingerprint = CP.getCarFingerprint(); + auto carName = CP.getCarName(); + auto safetyConfigs = CP.getSafetyConfigs(); + auto safetyModel = safetyConfigs[0].getSafetyModel(); + + hasAutoTune = (carName == "hyundai" || carName == "toyota") && CP.getLateralTuning().which() == cereal::CarParams::LateralTuning::TORQUE; + uiState()->scene.has_auto_tune = hasAutoTune; + hasCommaNNFFSupport = checkCommaNNFFSupport(carFingerprint); + hasDashSpeedLimits = carName == "hyundai" || carName == "toyota"; + hasNNFFLog = checkNNFFLogFileExists(carFingerprint); + hasOpenpilotLongitudinal = CP.getOpenpilotLongitudinalControl() && !params.getBool("DisableOpenpilotLongitudinal"); + hasPCMCruise = CP.getPcmCruise(); + isGM = carName == "gm"; + isHKGCanFd = carName == "hyundai" && safetyModel == cereal::CarParams::SafetyModel::HYUNDAI_CANFD; + isToyota = carName == "toyota"; + steerRatioStock = CP.getSteerRatio(); + + steerRatioToggle->setTitle(QString(tr("Steer Ratio (Default: %1)")).arg(QString::number(steerRatioStock, 'f', 2))); + steerRatioToggle->updateControl(steerRatioStock * 0.75, steerRatioStock * 1.25, "", 0.01); + steerRatioToggle->refresh(); + } else { + hasAutoTune = false; + hasCommaNNFFSupport = false; + hasDashSpeedLimits = true; + hasNNFFLog = true; + hasOpenpilotLongitudinal = true; + hasPCMCruise = true; + isGM = true; + isHKGCanFd = true; + isToyota = true; + } + + hideToggles(); +} + +void FrogPilotControlsPanel::updateMetric() { + bool previousIsMetric = isMetric; + isMetric = params.getBool("IsMetric"); + + if (isMetric != previousIsMetric) { + double distanceConversion = isMetric ? FOOT_TO_METER : METER_TO_FOOT; + double speedConversion = isMetric ? MILE_TO_KM : KM_TO_MILE; + + params.putIntNonBlocking("LaneDetectionWidth", std::nearbyint(params.getInt("LaneDetectionWidth") * distanceConversion)); + params.putIntNonBlocking("StoppingDistance", std::nearbyint(params.getInt("StoppingDistance") * distanceConversion)); + + params.putIntNonBlocking("CESpeed", std::nearbyint(params.getInt("CESpeed") * speedConversion)); + params.putIntNonBlocking("CESpeedLead", std::nearbyint(params.getInt("CESpeedLead") * speedConversion)); + params.putIntNonBlocking("CustomCruise", std::nearbyint(params.getInt("CustomCruise") * speedConversion)); + params.putIntNonBlocking("CustomCruiseLong", std::nearbyint(params.getInt("CustomCruiseLong") * speedConversion)); + params.putIntNonBlocking("MinimumLaneChangeSpeed", std::nearbyint(params.getInt("MinimumLaneChangeSpeed") * speedConversion)); + params.putIntNonBlocking("Offset1", std::nearbyint(params.getInt("Offset1") * speedConversion)); + params.putIntNonBlocking("Offset2", std::nearbyint(params.getInt("Offset2") * speedConversion)); + params.putIntNonBlocking("Offset3", std::nearbyint(params.getInt("Offset3") * speedConversion)); + params.putIntNonBlocking("Offset4", std::nearbyint(params.getInt("Offset4") * speedConversion)); + params.putIntNonBlocking("PauseAOLOnBrake", std::nearbyint(params.getInt("PauseAOLOnBrake") * speedConversion)); + params.putIntNonBlocking("PauseLateralOnSignal", std::nearbyint(params.getInt("PauseLateralOnSignal") * speedConversion)); + params.putIntNonBlocking("PauseLateralSpeed", std::nearbyint(params.getInt("PauseLateralSpeed") * speedConversion)); + params.putIntNonBlocking("SetSpeedOffset", std::nearbyint(params.getInt("SetSpeedOffset") * speedConversion)); + } + + FrogPilotParamValueControl *customCruiseToggle = static_cast(toggles["CustomCruise"]); + FrogPilotParamValueControl *customCruiseLongToggle = static_cast(toggles["CustomCruiseLong"]); + FrogPilotParamValueControl *laneWidthToggle = static_cast(toggles["LaneDetectionWidth"]); + FrogPilotParamValueControl *minimumLaneChangeSpeedToggle = static_cast(toggles["MinimumLaneChangeSpeed"]); + FrogPilotParamValueControl *offset1Toggle = static_cast(toggles["Offset1"]); + FrogPilotParamValueControl *offset2Toggle = static_cast(toggles["Offset2"]); + FrogPilotParamValueControl *offset3Toggle = static_cast(toggles["Offset3"]); + FrogPilotParamValueControl *offset4Toggle = static_cast(toggles["Offset4"]); + FrogPilotParamValueControl *pauseAOLOnBrakeToggle = static_cast(toggles["PauseAOLOnBrake"]); + FrogPilotParamValueControl *pauseLateralToggle = static_cast(toggles["PauseLateralSpeed"]); + FrogPilotParamValueControl *setSpeedOffsetToggle = static_cast(toggles["SetSpeedOffset"]); + FrogPilotParamValueControl *stoppingDistanceToggle = static_cast(toggles["StoppingDistance"]); + + if (isMetric) { + offset1Toggle->setTitle(tr("Speed Limit Offset (0-34 kph)")); + offset2Toggle->setTitle(tr("Speed Limit Offset (35-54 kph)")); + offset3Toggle->setTitle(tr("Speed Limit Offset (55-64 kph)")); + offset4Toggle->setTitle(tr("Speed Limit Offset (65-99 kph)")); + + offset1Toggle->setDescription(tr("Set speed limit offset for limits between 0-34 kph.")); + offset2Toggle->setDescription(tr("Set speed limit offset for limits between 35-54 kph.")); + offset3Toggle->setDescription(tr("Set speed limit offset for limits between 55-64 kph.")); + offset4Toggle->setDescription(tr("Set speed limit offset for limits between 65-99 kph.")); + + customCruiseToggle->updateControl(1, 150, tr(" kph")); + customCruiseLongToggle->updateControl(1, 150, tr(" kph")); + minimumLaneChangeSpeedToggle->updateControl(0, 150, tr(" kph")); + offset1Toggle->updateControl(-99, 99, tr(" kph")); + offset2Toggle->updateControl(-99, 99, tr(" kph")); + offset3Toggle->updateControl(-99, 99, tr(" kph")); + offset4Toggle->updateControl(-99, 99, tr(" kph")); + pauseAOLOnBrakeToggle->updateControl(0, 99, tr(" kph")); + pauseLateralToggle->updateControl(0, 99, tr(" kph")); + setSpeedOffsetToggle->updateControl(0, 150, tr(" kph")); + + laneWidthToggle->updateControl(0, 30, tr(" meters"), 10); + stoppingDistanceToggle->updateControl(0, 5, tr(" meters")); + } else { + offset1Toggle->setTitle(tr("Speed Limit Offset (0-34 mph)")); + offset2Toggle->setTitle(tr("Speed Limit Offset (35-54 mph)")); + offset3Toggle->setTitle(tr("Speed Limit Offset (55-64 mph)")); + offset4Toggle->setTitle(tr("Speed Limit Offset (65-99 mph)")); + + offset1Toggle->setDescription(tr("Set speed limit offset for limits between 0-34 mph.")); + offset2Toggle->setDescription(tr("Set speed limit offset for limits between 35-54 mph.")); + offset3Toggle->setDescription(tr("Set speed limit offset for limits between 55-64 mph.")); + offset4Toggle->setDescription(tr("Set speed limit offset for limits between 65-99 mph.")); + + customCruiseToggle->updateControl(1, 99, tr(" mph")); + customCruiseLongToggle->updateControl(1, 99, tr(" mph")); + minimumLaneChangeSpeedToggle->updateControl(0, 99, tr(" mph")); + offset1Toggle->updateControl(-99, 99, tr(" mph")); + offset2Toggle->updateControl(-99, 99, tr(" mph")); + offset3Toggle->updateControl(-99, 99, tr(" mph")); + offset4Toggle->updateControl(-99, 99, tr(" mph")); + pauseAOLOnBrakeToggle->updateControl(0, 99, tr(" mph")); + pauseLateralToggle->updateControl(0, 99, tr(" mph")); + setSpeedOffsetToggle->updateControl(0, 99, tr(" mph")); + + laneWidthToggle->updateControl(0, 100, tr(" feet"), 10); + stoppingDistanceToggle->updateControl(0, 10, tr(" feet")); + } + + customCruiseToggle->refresh(); + customCruiseLongToggle->refresh(); + laneWidthToggle->refresh(); + minimumLaneChangeSpeedToggle->refresh(); + offset1Toggle->refresh(); + offset2Toggle->refresh(); + offset3Toggle->refresh(); + offset4Toggle->refresh(); + pauseAOLOnBrakeToggle->refresh(); + pauseLateralToggle->refresh(); + setSpeedOffsetToggle->refresh(); + stoppingDistanceToggle->refresh(); +} + +void FrogPilotControlsPanel::hideToggles() { + customPersonalitiesOpen = false; + slcOpen = false; + + conditionalSpeedsImperial->setVisible(false); + conditionalSpeedsMetric->setVisible(false); + deleteModelBtn->setVisible(false); + downloadModelBtn->setVisible(false); + personalitiesInfoBtn->setVisible(false); + selectModelBtn->setVisible(false); + + std::set longitudinalKeys = {"ConditionalExperimental", "DrivingPersonalities", "ExperimentalModeActivation", + "LongitudinalTune", "MTSCEnabled", "SpeedLimitController", "VisionTurnControl"}; + + for (auto &[key, toggle] : toggles) { + toggle->setVisible(false); + + if (!hasOpenpilotLongitudinal && longitudinalKeys.find(key.c_str()) != longitudinalKeys.end()) { + continue; + } + + bool subToggles = aggressivePersonalityKeys.find(key.c_str()) != aggressivePersonalityKeys.end() || + aolKeys.find(key.c_str()) != aolKeys.end() || + conditionalExperimentalKeys.find(key.c_str()) != conditionalExperimentalKeys.end() || + customdrivingPersonalityKeys.find(key.c_str()) != customdrivingPersonalityKeys.end() || + deviceManagementKeys.find(key.c_str()) != deviceManagementKeys.end() || + drivingPersonalityKeys.find(key.c_str()) != drivingPersonalityKeys.end() || + experimentalModeActivationKeys.find(key.c_str()) != experimentalModeActivationKeys.end() || + laneChangeKeys.find(key.c_str()) != laneChangeKeys.end() || + lateralTuneKeys.find(key.c_str()) != lateralTuneKeys.end() || + longitudinalTuneKeys.find(key.c_str()) != longitudinalTuneKeys.end() || + mtscKeys.find(key.c_str()) != mtscKeys.end() || + qolKeys.find(key.c_str()) != qolKeys.end() || + relaxedPersonalityKeys.find(key.c_str()) != relaxedPersonalityKeys.end() || + speedLimitControllerKeys.find(key.c_str()) != speedLimitControllerKeys.end() || + speedLimitControllerControlsKeys.find(key.c_str()) != speedLimitControllerControlsKeys.end() || + speedLimitControllerQOLKeys.find(key.c_str()) != speedLimitControllerQOLKeys.end() || + speedLimitControllerVisualsKeys.find(key.c_str()) != speedLimitControllerVisualsKeys.end() || + standardPersonalityKeys.find(key.c_str()) != standardPersonalityKeys.end() || + trafficPersonalityKeys.find(key.c_str()) != trafficPersonalityKeys.end() || + visionTurnControlKeys.find(key.c_str()) != visionTurnControlKeys.end(); + toggle->setVisible(!subToggles); + } + + update(); +} + +void FrogPilotControlsPanel::hideSubToggles() { + if (customPersonalitiesOpen) { + for (auto &[key, toggle] : toggles) { + bool isVisible = drivingPersonalityKeys.find(key.c_str()) != drivingPersonalityKeys.end(); + toggle->setVisible(isVisible); + } + } else if (slcOpen) { + for (auto &[key, toggle] : toggles) { + bool isVisible = speedLimitControllerKeys.find(key.c_str()) != speedLimitControllerKeys.end(); + toggle->setVisible(isVisible); + } + } + + update(); +} + +void FrogPilotControlsPanel::hideSubSubToggles() { + personalitiesInfoBtn->setVisible(false); + + for (auto &[key, toggle] : toggles) { + bool isVisible = customdrivingPersonalityKeys.find(key.c_str()) != customdrivingPersonalityKeys.end(); + toggle->setVisible(isVisible); + } + + update(); +} diff --git a/selfdrive/frogpilot/ui/qt/offroad/control_settings.h b/selfdrive/frogpilot/ui/qt/offroad/control_settings.h new file mode 100644 index 0000000000..b1eee2170f --- /dev/null +++ b/selfdrive/frogpilot/ui/qt/offroad/control_settings.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include "selfdrive/ui/qt/offroad/settings.h" +#include "selfdrive/ui/ui.h" + +class FrogPilotControlsPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotControlsPanel(SettingsWindow *parent); + +signals: + void openParentToggle(); + void openSubParentToggle(); + void openSubSubParentToggle(); + +private: + void hideSubToggles(); + void hideSubSubToggles(); + void hideToggles(); + void showEvent(QShowEvent *event, const UIState &s); + void updateCarToggles(); + void updateMetric(); + void updateState(const UIState &s); + + ButtonControl *deleteModelBtn; + ButtonControl *downloadModelBtn; + ButtonControl *personalitiesInfoBtn; + ButtonControl *selectModelBtn; + + FrogPilotDualParamControl *conditionalSpeedsImperial; + FrogPilotDualParamControl *conditionalSpeedsMetric; + + FrogPilotParamManageControl *modelManagerToggle; + + FrogPilotParamValueToggleControl *steerRatioToggle; + + std::set aggressivePersonalityKeys = {"AggressiveFollow", "AggressiveJerkAcceleration", "AggressiveJerkDanger", "AggressiveJerkSpeed", "ResetAggressivePersonality"}; + std::set aolKeys = {"AlwaysOnLateralLKAS", "AlwaysOnLateralMain", "HideAOLStatusBar", "PauseAOLOnBrake"}; + std::set conditionalExperimentalKeys = {"CECurves", "CELead", "CENavigation", "CESignal", "CEStopLights", "HideCEMStatusBar"}; + std::set customdrivingPersonalityKeys = {"AggressivePersonalityProfile", "RelaxedPersonalityProfile", "StandardPersonalityProfile", "TrafficPersonalityProfile"}; + std::set deviceManagementKeys = {"DeviceShutdown", "IncreaseThermalLimits", "LowVoltageShutdown", "NoLogging", "NoUploads", "OfflineMode"}; + std::set drivingPersonalityKeys = {"CustomPersonalities", "OnroadDistanceButton"}; + std::set experimentalModeActivationKeys = {"ExperimentalModeViaDistance", "ExperimentalModeViaLKAS", "ExperimentalModeViaTap"}; + std::set laneChangeKeys = {"LaneChangeTime", "LaneDetectionWidth", "MinimumLaneChangeSpeed", "NudgelessLaneChange", "OneLaneChange"}; + std::set lateralTuneKeys = {"ForceAutoTune", "NNFF", "NNFFLite", "SteerRatio", "TacoTune", "TurnDesires"}; + std::set longitudinalTuneKeys = {"AccelerationProfile", "AggressiveAcceleration", "DecelerationProfile", "LeadDetectionThreshold", "SmoothBraking", "StoppingDistance", "TrafficMode"}; + std::set mtscKeys = {"DisableMTSCSmoothing", "MTSCAggressiveness", "MTSCCurvatureCheck"}; + std::set qolKeys = {"CustomCruise", "CustomCruiseLong", "ForceStandstill", "MapGears", "PauseLateralSpeed", "ReverseCruise", "SetSpeedOffset"}; + std::set relaxedPersonalityKeys = {"RelaxedFollow", "RelaxedJerkAcceleration", "RelaxedJerkDanger", "RelaxedJerkSpeed", "ResetRelaxedPersonality"}; + std::set speedLimitControllerKeys = {"SLCControls", "SLCQOL", "SLCVisuals"}; + std::set speedLimitControllerControlsKeys = {"Offset1", "Offset2", "Offset3", "Offset4", "SLCFallback", "SLCOverride", "SLCPriority"}; + std::set speedLimitControllerQOLKeys = {"ForceMPHDashboard", "SetSpeedLimit", "SLCConfirmation", "SLCLookaheadHigher", "SLCLookaheadLower"}; + std::set speedLimitControllerVisualsKeys = {"ShowSLCOffset", "SpeedLimitChangedAlert", "UseVienna"}; + std::set standardPersonalityKeys = {"StandardFollow", "StandardJerkAcceleration", "StandardJerkDanger", "StandardJerkSpeed", "ResetStandardPersonality"}; + std::set trafficPersonalityKeys = {"TrafficFollow", "TrafficJerkAcceleration", "TrafficJerkDanger", "TrafficJerkSpeed", "ResetTrafficPersonality"}; + std::set visionTurnControlKeys = {"CurveSensitivity", "DisableVTSCSmoothing", "TurnAggressiveness"}; + + std::map toggles; + + Params params; + Params paramsMemory{"/dev/shm/params"}; + + bool customPersonalitiesOpen; + bool hasAutoTune; + bool hasCommaNNFFSupport; + bool hasNNFFLog; + bool hasOpenpilotLongitudinal; + bool hasPCMCruise; + bool hasDashSpeedLimits; + bool isGM; + bool isHKGCanFd; + bool isMetric = params.getBool("IsMetric"); + bool isRelease; + bool isToyota; + bool slcOpen; + bool started; + + float steerRatioStock; +}; diff --git a/selfdrive/frogpilot/ui/qt/offroad/personalities_info.txt b/selfdrive/frogpilot/ui/qt/offroad/personalities_info.txt new file mode 100644 index 0000000000..d73cea3c97 --- /dev/null +++ b/selfdrive/frogpilot/ui/qt/offroad/personalities_info.txt @@ -0,0 +1,23 @@ +Following Distance: +Represents the desired time-based distance from the lead vehicle in seconds. This value adjusts how closely openpilot follows the car in front. It is not an exact physical distance but a representative value in terms of time. + +Real-World Example: +If this value is set to 1.5 seconds, and you are driving at 50 mph (approximately 80 kph), openpilot aims to keep a distance of approximately 110 feet (about 33.5 meters) from the lead vehicle. This means it will maintain a time gap of 1.5 seconds at any given speed. + +Acceleration/Deceleration Response: +Controls how aggressively openpilot adjusts the rate of acceleration and deceleration. Increasing this value makes it more "costly" to change the rate at which the vehicle accelerates or decelerates, leading to smoother transitions. This primarily affects the vehicle's behavior during changes in speed, such as when speeding up or slowing down. + +Real-World Example: +If you prefer a smoother driving experience when your car changes speed, you can increase this value. For instance, with a higher value, when following a lead vehicle that suddenly accelerates or decelerates, openpilot will take more time to match the lead vehicle's speed, resulting in a gentler ride. + +Danger Zone Response: +Adjusts how aggressively enters the "danger zone" state. Increasing this value makes it more cautious when approaching lead vehicles. This setting affects how the vehicle responds to heavier traffic conditions. + +Real-World Example: +If you are approaching a much slower car farther ahead, setting a higher danger zone will cause openpilot to be more cautious and brake sooner and harder the higher the speed difference is and the smaller the distance is. + +Speed Control Response: +Adjusts how aggressively openpilot changes the vehicle's speed overall. Increasing this value makes it more "costly" to adjust the speed, leading to more gradual changes. This setting affects how the vehicle responds to traffic conditions and set cruise speeds. + +Real-World Example: +If you are driving in a city with frequent stop-and-go traffic, setting a higher speed jerk factor will make openpilot's speed adjustments smoother. For example, when accelerating to match the set cruise speed or slowing down due to traffic, openpilot will make these speed changes more gradually, providing a smoother overall driving experience. \ No newline at end of file diff --git a/selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.cc b/selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.cc new file mode 100644 index 0000000000..8b68df57d1 --- /dev/null +++ b/selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.cc @@ -0,0 +1,307 @@ +#include +#include +#include + +#include "selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.h" + +QStringList getCarNames(const QString &carMake, QMap &carModels) { + QMap makeMap; + makeMap["acura"] = "honda"; + makeMap["audi"] = "volkswagen"; + makeMap["buick"] = "gm"; + makeMap["cadillac"] = "gm"; + makeMap["chevrolet"] = "gm"; + makeMap["chrysler"] = "chrysler"; + makeMap["dodge"] = "chrysler"; + makeMap["ford"] = "ford"; + makeMap["genesis"] = "hyundai"; + makeMap["gmc"] = "gm"; + makeMap["holden"] = "gm"; + makeMap["honda"] = "honda"; + makeMap["hyundai"] = "hyundai"; + makeMap["jeep"] = "chrysler"; + makeMap["kia"] = "hyundai"; + makeMap["lexus"] = "toyota"; + makeMap["lincoln"] = "ford"; + makeMap["man"] = "volkswagen"; + makeMap["mazda"] = "mazda"; + makeMap["nissan"] = "nissan"; + makeMap["ram"] = "chrysler"; + makeMap["seat"] = "volkswagen"; + makeMap["škoda"] = "volkswagen"; + makeMap["subaru"] = "subaru"; + makeMap["tesla"] = "tesla"; + makeMap["toyota"] = "toyota"; + makeMap["volkswagen"] = "volkswagen"; + + QString targetFolder = makeMap.value(carMake, carMake); + QFile file(QString("../car/%1/values.py").arg(targetFolder)); + QStringList names; + QSet uniqueNames; + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return names; + + QTextStream in(&file); + QString fileContent = in.readAll(); + file.close(); + + fileContent.remove(QRegularExpression("#[^\n]*")); + + QRegularExpression carModelRegex(R"delimiter((\w+)\s*=\s*\w+\s*\(\s*\[([\s\S]*?)\]\s*,)delimiter"); + QRegularExpression carDocsRegex(R"delimiter(CarDocs\(\s*"([^"]+)"[^)]*\))delimiter"); + + QRegularExpressionMatchIterator carModelIt = carModelRegex.globalMatch(fileContent); + while (carModelIt.hasNext()) { + QRegularExpressionMatch carModelMatch = carModelIt.next(); + QString platform = carModelMatch.captured(1); + QString platformSection = carModelMatch.captured(2); + + QRegularExpressionMatchIterator carDocsIt = carDocsRegex.globalMatch(platformSection); + while (carDocsIt.hasNext()) { + QRegularExpressionMatch match = carDocsIt.next(); + QString carName = match.captured(1); + + if (carName.contains(QRegularExpression("^[A-Za-z0-9 .()-]+$")) && carName.count(" ") >= 1) { + QStringList nameParts = carName.split(" "); + if (nameParts.contains(carMake, Qt::CaseInsensitive)) { + if (!uniqueNames.contains(carName)) { + names << carName; + carModels[carName] = platform; + uniqueNames.insert(carName); + } + } + } + } + } + + std::sort(names.begin(), names.end()); + return names; +} + +FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(SettingsWindow *parent) : FrogPilotListWidget(parent) { + selectMakeButton = new ButtonControl(tr("Select Make"), tr("SELECT")); + QObject::connect(selectMakeButton, &ButtonControl::clicked, [this]() { + QStringList makes = { + "Acura", "Audi", "Buick", "Cadillac", "Chevrolet", "Chrysler", "Dodge", "Ford", "Genesis", + "GMC", "Holden", "Honda", "Hyundai", "Jeep", "Kia", "Lexus", "Lincoln", "MAN", "Mazda", + "Nissan", "Ram", "SEAT", "Škoda", "Subaru", "Tesla", "Toyota", "Volkswagen", + }; + + QString newMakeSelection = MultiOptionDialog::getSelection(tr("Select a Make"), makes, "", this); + if (!newMakeSelection.isEmpty()) { + carMake = newMakeSelection; + params.putNonBlocking("CarMake", carMake.toStdString()); + selectMakeButton->setValue(newMakeSelection); + setModels(); + } + }); + addItem(selectMakeButton); + + selectModelButton = new ButtonControl(tr("Select Model"), tr("SELECT")); + QObject::connect(selectModelButton, &ButtonControl::clicked, [this]() { + QString newModelSelection = MultiOptionDialog::getSelection(tr("Select a Model"), models, "", this); + if (!newModelSelection.isEmpty()) { + carModel = newModelSelection; + QString modelIdentifier = carModels.value(newModelSelection); + params.putNonBlocking("CarModel", modelIdentifier.toStdString()); + params.putNonBlocking("CarModelName", newModelSelection.toStdString()); + selectModelButton->setValue(newModelSelection); + } + }); + addItem(selectModelButton); + selectModelButton->setVisible(false); + + ParamControl *forceFingerprint = new ParamControl("ForceFingerprint", tr("Disable Automatic Fingerprint Detection"), tr("Forces the selected fingerprint and prevents it from ever changing."), "", this); + addItem(forceFingerprint); + + bool disableOpenpilotLongState = params.getBool("DisableOpenpilotLongitudinal"); + disableOpenpilotLong = new ToggleControl(tr("Disable openpilot Longitudinal Control"), tr("Disable openpilot longitudinal control and use stock ACC instead."), "", disableOpenpilotLongState); + QObject::connect(disableOpenpilotLong, &ToggleControl::toggleFlipped, [this](bool state) { + if (state) { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely disable openpilot longitudinal control?"), this)) { + params.putBool("DisableOpenpilotLongitudinal", state); + if (started) { + if (FrogPilotConfirmationDialog::toggle(tr("Reboot required to take effect."), tr("Reboot Now"), this)) { + Hardware::reboot(); + } + } + } else { + disableOpenpilotLong->refresh(); + } + } else { + params.putBool("DisableOpenpilotLongitudinal", state); + } + updateCarToggles(); + }); + addItem(disableOpenpilotLong); + + std::vector> vehicleToggles { + {"LongPitch", tr("Long Pitch Compensation"), tr("Smoothen out the gas and pedal controls."), ""}, + {"VoltSNG", tr("2017 Volt SNG"), tr("Enable the 'Stop and Go' hack for 2017 Chevy Volts."), ""}, + + {"CrosstrekTorque", tr("Subaru Crosstrek Torque Increase"), tr("Increases the maximum allowed torque for the Subaru Crosstrek."), ""}, + + {"ToyotaDoors", tr("Automatically Lock/Unlock Doors"), tr("Automatically lock the doors when in drive and unlock when in park."), ""}, + {"ClusterOffset", tr("Cluster Offset"), tr("Set the cluster offset openpilot uses to try and match the speed displayed on the dash."), ""}, + {"SNGHack", tr("Stop and Go Hack"), tr("Enable the 'Stop and Go' hack for vehicles without stock stop and go functionality."), ""}, + {"ToyotaTune", tr("Toyota Tune"), tr("Use a custom Toyota longitudinal tune.\n\nCydia = More focused on TSS-P vehicles but works for all Toyotas\n\nDragonPilot = Focused on TSS2 vehicles\n\nFrogPilot = Takes the best of both worlds with some personal tweaks focused around FrogsGoMoo's 2019 Lexus ES 350"), ""}, + }; + + for (const auto &[param, title, desc, icon] : vehicleToggles) { + AbstractControl *vehicleToggle; + + if (param == "ToyotaDoors") { + std::vector lockToggles{"LockDoors", "UnlockDoors"}; + std::vector lockToggleNames{tr("Lock"), tr("Unlock")}; + vehicleToggle = new FrogPilotParamToggleControl(param, title, desc, icon, lockToggles, lockToggleNames); + + } else if (param == "ClusterOffset") { + vehicleToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1.000, 1.050, std::map(), this, false, "x", 1, 0.001); + + } else if (param == "ToyotaTune") { + std::vector> tuneOptions{ + {"StockTune", tr("Stock")}, + {"CydiaTune", tr("Cydia")}, + {"DragonPilotTune", tr("DragonPilot")}, + {"FrogsGoMooTune", tr("FrogPilot")}, + }; + + FrogPilotButtonsParamControl *toyotaTuneToggle = new FrogPilotButtonsParamControl(param, title, desc, icon, tuneOptions); + vehicleToggle = toyotaTuneToggle; + + QObject::connect(toyotaTuneToggle, &FrogPilotButtonsParamControl::buttonClicked, [this]() { + if (started) { + if (FrogPilotConfirmationDialog::toggle(tr("Reboot required to take effect."), tr("Reboot Now"), this)) { + Hardware::reboot(); + } + } + }); + + } else { + vehicleToggle = new ParamControl(param, title, desc, icon, this); + } + + vehicleToggle->setVisible(false); + addItem(vehicleToggle); + toggles[param.toStdString()] = vehicleToggle; + + QObject::connect(static_cast(vehicleToggle), &ToggleControl::toggleFlipped, &updateFrogPilotToggles); + QObject::connect(static_cast(vehicleToggle), &FrogPilotParamToggleControl::buttonTypeClicked, &updateFrogPilotToggles); + + QObject::connect(vehicleToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + std::set rebootKeys = {"CrosstrekTorque"}; + for (const QString &key : rebootKeys) { + QObject::connect(static_cast(toggles[key.toStdString().c_str()]), &ToggleControl::toggleFlipped, [this]() { + if (started) { + if (FrogPilotConfirmationDialog::toggle(tr("Reboot required to take effect."), tr("Reboot Now"), this)) { + Hardware::reboot(); + } + } + }); + } + + QObject::connect(uiState(), &UIState::offroadTransition, [this]() { + std::thread([this]() { + while (carMake.isEmpty()) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + carMake = QString::fromStdString(params.get("CarMake")); + carModel = QString::fromStdString(params.get(params.get("CarModelName").empty() ? "CarModel" : "CarModelName")); + } + setModels(); + updateCarToggles(); + }).detach(); + }); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotVehiclesPanel::updateState); + + carMake = QString::fromStdString(params.get("CarMake")); + carModel = QString::fromStdString(params.get(params.get("CarModelName").empty() ? "CarModel" : "CarModelName")); + + if (!carMake.isEmpty()) { + setModels(); + } +} + +void FrogPilotVehiclesPanel::updateState(const UIState &s) { + if (!isVisible()) return; + + started = s.scene.started; +} + +void FrogPilotVehiclesPanel::updateCarToggles() { + auto carParams = params.get("CarParamsPersistent"); + if (!carParams.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(carParams.data(), carParams.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + auto carFingerprint = CP.getCarFingerprint(); + + hasExperimentalOpenpilotLongitudinal = CP.getExperimentalLongitudinalAvailable(); + hasOpenpilotLongitudinal = CP.getOpenpilotLongitudinalControl() && !params.getBool("DisableOpenpilotLongitudinal"); + hasSNG = CP.getMinEnableSpeed() <= 0; + isGMPCMCruise = CP.getCarName() == "gm" && CP.getPcmCruise(); + isImpreza = carFingerprint == "SUBARU_IMPREZA"; + } else { + hasExperimentalOpenpilotLongitudinal = false; + hasOpenpilotLongitudinal = true; + hasSNG = false; + isImpreza = true; + } + + hideToggles(); +} + +void FrogPilotVehiclesPanel::setModels() { + models = getCarNames(carMake.toLower(), carModels); + hideToggles(); +} + +void FrogPilotVehiclesPanel::hideToggles() { + disableOpenpilotLong->setVisible(hasOpenpilotLongitudinal && !hasExperimentalOpenpilotLongitudinal && !isGMPCMCruise || params.getBool("DisableOpenpilotLongitudinal")); + + selectMakeButton->setValue(carMake); + selectModelButton->setValue(carModel); + selectModelButton->setVisible(!carMake.isEmpty()); + + bool gm = carMake == "Buick" || carMake == "Cadillac" || carMake == "Chevrolet" || carMake == "GM" || carMake == "GMC"; + bool subaru = carMake == "Subaru"; + bool toyota = carMake == "Lexus" || carMake == "Toyota"; + + std::set imprezaKeys = {"CrosstrekTorque"}; + std::set longitudinalKeys = {"ToyotaTune", "LongPitch", "SNGHack"}; + std::set sngKeys = {"SNGHack"}; + + for (auto &[key, toggle] : toggles) { + if (toggle) { + toggle->setVisible(false); + + if ((!hasOpenpilotLongitudinal || params.getBool("DisableOpenpilotLongitudinal")) && longitudinalKeys.find(key.c_str()) != longitudinalKeys.end()) { + continue; + } + + if (hasSNG && sngKeys.find(key.c_str()) != sngKeys.end()) { + continue; + } + + if (!isImpreza && imprezaKeys.find(key.c_str()) != imprezaKeys.end()) { + continue; + } + + if (gm) { + toggle->setVisible(gmKeys.find(key.c_str()) != gmKeys.end()); + } else if (subaru) { + toggle->setVisible(subaruKeys.find(key.c_str()) != subaruKeys.end()); + } else if (toyota) { + toggle->setVisible(toyotaKeys.find(key.c_str()) != toyotaKeys.end()); + } + } + } + + update(); +} diff --git a/selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.h b/selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.h new file mode 100644 index 0000000000..d01fea3999 --- /dev/null +++ b/selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include + +#include "selfdrive/ui/qt/offroad/settings.h" +#include "selfdrive/ui/ui.h" + +class FrogPilotVehiclesPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotVehiclesPanel(SettingsWindow *parent); + +private: + void hideToggles(); + void setModels(); + void updateCarToggles(); + void updateState(const UIState &s); + + ButtonControl *selectMakeButton; + ButtonControl *selectModelButton; + + ToggleControl *disableOpenpilotLong; + + QString carMake; + QString carModel; + + QStringList models; + + QMap carModels; + + std::set gmKeys = {"LongPitch", "VoltSNG"}; + std::set subaruKeys = {"CrosstrekTorque"}; + std::set toyotaKeys = {"ClusterOffset", "SNGHack", "ToyotaDoors", "ToyotaTune"}; + + std::map toggles; + + Params params; + + bool hasExperimentalOpenpilotLongitudinal; + bool hasOpenpilotLongitudinal; + bool hasSNG; + bool isGMPCMCruise; + bool isImpreza; + bool started; +}; diff --git a/selfdrive/frogpilot/ui/qt/offroad/visual_settings.cc b/selfdrive/frogpilot/ui/qt/offroad/visual_settings.cc new file mode 100644 index 0000000000..a19399d06f --- /dev/null +++ b/selfdrive/frogpilot/ui/qt/offroad/visual_settings.cc @@ -0,0 +1,452 @@ +#include "selfdrive/frogpilot/ui/qt/offroad/visual_settings.h" + +FrogPilotVisualsPanel::FrogPilotVisualsPanel(SettingsWindow *parent) : FrogPilotListWidget(parent) { + std::string branch = params.get("GitBranch"); + isRelease = branch == "FrogPilot"; + + const std::vector> visualToggles { + {"AlertVolumeControl", tr("Alert Volume Controller"), tr("Control the volume level for each individual sound in openpilot."), "../frogpilot/assets/toggle_icons/icon_mute.png"}, + {"DisengageVolume", tr("Disengage Volume"), tr("Related alerts:\n\nAdaptive Cruise Disabled\nParking Brake Engaged\nBrake Pedal Pressed\nSpeed too Low"), ""}, + {"EngageVolume", tr("Engage Volume"), tr("Related alerts:\n\nNNFF Torque Controller loaded\nopenpilot engaged"), ""}, + {"PromptVolume", tr("Prompt Volume"), tr("Related alerts:\n\nCar Detected in Blindspot\nSpeed too Low\nSteer Unavailable Below 'X'\nTake Control, Turn Exceeds Steering Limit"), ""}, + {"PromptDistractedVolume", tr("Prompt Distracted Volume"), tr("Related alerts:\n\nPay Attention, Driver Distracted\nTouch Steering Wheel, Driver Unresponsive"), ""}, + {"RefuseVolume", tr("Refuse Volume"), tr("Related alerts:\n\nopenpilot Unavailable"), ""}, + {"WarningSoftVolume", tr("Warning Soft Volume"), tr("Related alerts:\n\nBRAKE!, Risk of Collision\nTAKE CONTROL IMMEDIATELY"), ""}, + {"WarningImmediateVolume", tr("Warning Immediate Volume"), tr("Related alerts:\n\nDISENGAGE IMMEDIATELY, Driver Distracted\nDISENGAGE IMMEDIATELY, Driver Unresponsive"), ""}, + + {"CustomAlerts", tr("Custom Alerts"), tr("Enable custom alerts for openpilot events."), "../frogpilot/assets/toggle_icons/icon_green_light.png"}, + {"GreenLightAlert", tr("Green Light Alert"), tr("Get an alert when a traffic light changes from red to green."), ""}, + {"LeadDepartingAlert", tr("Lead Departing Alert"), tr("Get an alert when the lead vehicle starts departing when at a standstill."), ""}, + {"LoudBlindspotAlert", tr("Loud Blindspot Alert"), tr("Enable a louder alert for when a vehicle is detected in the blindspot when attempting to change lanes."), ""}, + + {"CustomUI", tr("Custom Onroad UI"), tr("Customize the Onroad UI."), "../assets/offroad/icon_road.png"}, + {"Compass", tr("Compass"), tr("Add a compass to the onroad UI."), ""}, + {"CustomPaths", tr("Paths"), tr("Show your projected acceleration on the driving path, detected adjacent lanes, or when a vehicle is detected in your blindspot."), ""}, + {"PedalsOnUI", tr("Pedals Being Pressed"), tr("Display the brake and gas pedals on the onroad UI below the steering wheel icon."), ""}, + {"RoadNameUI", tr("Road Name"), tr("Display the current road's name at the bottom of the screen. Sourced from OpenStreetMap."), ""}, + {"WheelIcon", tr("Steering Wheel Icon"), tr("Replace the default steering wheel icon with a custom icon."), ""}, + {"ShowStoppingPoint", tr("Stopping Points"), tr("Display the point where openpilot wants to stop for red lights/stop signs."), ""}, + + {"CustomTheme", tr("Custom Themes"), tr("Enable the ability to use custom themes."), "../frogpilot/assets/wheel_images/frog.png"}, + {"CustomColors", tr("Color Theme"), tr("Switch out the standard openpilot color scheme with themed colors.\n\nWant to submit your own color scheme? Post it in the 'feature-request' channel in the FrogPilot Discord!"), ""}, + {"CustomIcons", tr("Icon Pack"), tr("Switch out the standard openpilot icons with a set of themed icons.\n\nWant to submit your own icon pack? Post it in the 'feature-request' channel in the FrogPilot Discord!"), ""}, + {"CustomSounds", tr("Sound Pack"), tr("Switch out the standard openpilot sounds with a set of themed sounds.\n\nWant to submit your own sound pack? Post it in the 'feature-request' channel in the FrogPilot Discord!"), ""}, + {"CustomSignals", tr("Turn Signals"), tr("Add themed animation for your turn signals.\n\nWant to submit your own turn signal animation? Post it in the 'feature-request' channel in the FrogPilot Discord!"), ""}, + {"HolidayThemes", tr("Holiday Themes"), tr("The openpilot theme changes according to the current/upcoming holiday. Minor holidays last a day, while major holidays (Easter, Christmas, Halloween, etc.) last a week."), ""}, + {"RandomEvents", tr("Random Events"), tr("Enjoy a bit of unpredictability with random events that can occur during certain driving conditions. This is purely cosmetic and has no impact on driving controls!"), ""}, + + {"DeveloperUI", tr("Developer UI"), tr("Get various detailed information of what openpilot is doing behind the scenes."), "../frogpilot/assets/toggle_icons/icon_device.png"}, + {"BorderMetrics", tr("Border Metrics"), tr("Display metrics in onroad UI border."), ""}, + {"FPSCounter", tr("FPS Counter"), tr("Display the 'Frames Per Second' (FPS) of your onroad UI for monitoring system performance."), ""}, + {"LateralMetrics", tr("Lateral Metrics"), tr("Display various metrics related to the lateral performance of openpilot."), ""}, + {"LongitudinalMetrics", tr("Longitudinal Metrics"), tr("Display various metrics related to the longitudinal performance of openpilot."), ""}, + {"NumericalTemp", tr("Numerical Temperature Gauge"), tr("Replace the 'GOOD', 'OK', and 'HIGH' temperature statuses with a numerical temperature gauge based on the highest temperature between the memory, CPU, and GPU."), ""}, + {"SidebarMetrics", tr("Sidebar"), tr("Display various custom metrics on the sidebar for the CPU, GPU, RAM, IP, and storage used/left."), ""}, + {"UseSI", tr("Use International System of Units"), tr("Display relevant metrics in the SI format."), ""}, + + {"ModelUI", tr("Model UI"), tr("Customize the model visualizations on the screen."), "../assets/offroad/icon_calibration.png"}, + {"DynamicPathWidth", tr("Dynamic Path Width"), tr("Have the path width dynamically adjust based on the current engagement state of openpilot."), ""}, + {"HideLeadMarker", tr("Hide Lead Marker"), tr("Hide the lead marker from the onroad UI."), ""}, + {"LaneLinesWidth", tr("Lane Lines"), tr("Adjust the visual thickness of lane lines on your display.\n\nDefault matches the MUTCD average of 4 inches."), ""}, + {"PathEdgeWidth", tr("Path Edges"), tr("Adjust the width of the path edges shown on your UI to represent different driving modes and statuses.\n\nDefault is 20% of the total path.\n\nBlue = Navigation\nLight Blue = 'Always On Lateral'\nGreen = Default\nOrange = 'Experimental Mode'\nRed = 'Traffic Mode'\nYellow = 'Conditional Experimental Mode' Overridden"), ""}, + {"PathWidth", tr("Path Width"), tr("Customize the width of the driving path shown on your UI.\n\nDefault matches the width of a 2019 Lexus ES 350."), ""}, + {"RoadEdgesWidth", tr("Road Edges"), tr("Adjust the visual thickness of road edges on your display.\n\nDefault is 1/2 of the MUTCD average lane line width of 4 inches."), ""}, + {"UnlimitedLength", tr("'Unlimited' Road UI Length"), tr("Extend the display of the path, lane lines, and road edges out as far as the model can see."), ""}, + + {"QOLVisuals", tr("Quality of Life"), tr("Miscellaneous quality of life changes to improve your overall openpilot experience."), "../frogpilot/assets/toggle_icons/quality_of_life.png"}, + {"BigMap", tr("Big Map"), tr("Increase the size of the map in the onroad UI."), ""}, + {"CameraView", tr("Camera View"), tr("Choose your preferred camera view for the onroad UI. This is purely a visual change and doesn't impact how openpilot drives."), ""}, + {"DriverCamera", tr("Driver Camera On Reverse"), tr("Show the driver camera feed when in reverse."), ""}, + {"HideSpeed", tr("Hide Speed"), tr("Hide the speed indicator in the onroad UI. Additional toggle allows it to be hidden/shown via tapping the speed itself."), ""}, + {"MapStyle", tr("Map Style"), tr("Select a map style to use with navigation."), ""}, + {"StoppedTimer", tr("Stopped Timer"), tr("Display a timer in the onroad UI that indicates how long you've been stopped for."), ""}, + {"WheelSpeed", tr("Use Wheel Speed"), tr("Use the wheel speed instead of the cluster speed in the onroad UI."), ""}, + + {"ScreenManagement", tr("Screen Management"), tr("Manage your screen's brightness, timeout settings, and hide onroad UI elements."), "../frogpilot/assets/toggle_icons/icon_light.png"}, + {"HideUIElements", tr("Hide UI Elements"), tr("Hide the selected UI elements from the onroad screen."), ""}, + {"ScreenBrightness", tr("Screen Brightness"), tr("Customize your screen brightness when offroad."), ""}, + {"ScreenBrightnessOnroad", tr("Screen Brightness (Onroad)"), tr("Customize your screen brightness when onroad."), ""}, + {"ScreenRecorder", tr("Screen Recorder"), tr("Enable the ability to record the screen while onroad."), ""}, + {"ScreenTimeout", tr("Screen Timeout"), tr("Customize how long it takes for your screen to turn off."), ""}, + {"ScreenTimeoutOnroad", tr("Screen Timeout (Onroad)"), tr("Customize how long it takes for your screen to turn off when onroad."), ""}, + {"StandbyMode", tr("Standby Mode"), tr("Turn the screen off after your screen times out when onroad, but wake it back up when engagement state changes or important alerts are triggered."), ""}, + }; + + for (const auto &[param, title, desc, icon] : visualToggles) { + AbstractControl *visualToggle; + + if (param == "AlertVolumeControl") { + FrogPilotParamManageControl *alertVolumeControlToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(alertVolumeControlToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(alertVolumeControlKeys.find(key.c_str()) != alertVolumeControlKeys.end()); + } + }); + visualToggle = alertVolumeControlToggle; + } else if (alertVolumeControlKeys.find(param) != alertVolumeControlKeys.end()) { + if (param == "WarningImmediateVolume") { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 25, 100, std::map(), this, false, "%"); + } else { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 100, std::map(), this, false, "%"); + } + + } else if (param == "CustomAlerts") { + FrogPilotParamManageControl *customAlertsToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(customAlertsToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedCustomAlertsKeys = customAlertsKeys; + + if (!hasBSM) { + modifiedCustomAlertsKeys.erase("LoudBlindspotAlert"); + } + + toggle->setVisible(modifiedCustomAlertsKeys.find(key.c_str()) != modifiedCustomAlertsKeys.end()); + } + }); + visualToggle = customAlertsToggle; + + } else if (param == "CustomTheme") { + FrogPilotParamManageControl *customThemeToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(customThemeToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(customThemeKeys.find(key.c_str()) != customThemeKeys.end()); + } + }); + visualToggle = customThemeToggle; + } else if (param == "CustomColors" || param == "CustomIcons" || param == "CustomSignals" || param == "CustomSounds") { + std::vector themeOptions{tr("Stock"), tr("Frog"), tr("Tesla"), tr("Stalin")}; + FrogPilotButtonParamControl *themeSelection = new FrogPilotButtonParamControl(param, title, desc, icon, themeOptions); + visualToggle = themeSelection; + + if (param == "CustomSounds") { + QObject::connect(themeSelection, &FrogPilotButtonParamControl::buttonClicked, [this](int id) { + if (id == 1) { + if (FrogPilotConfirmationDialog::yesorno(tr("Do you want to enable the bonus 'Goat' sound effect?"), this)) { + params.putBoolNonBlocking("GoatScream", true); + } else { + params.putBoolNonBlocking("GoatScream", false); + } + } + }); + } + + } else if (param == "CustomUI") { + FrogPilotParamManageControl *customUIToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(customUIToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedCustomOnroadUIKeys = customOnroadUIKeys; + + if (!hasOpenpilotLongitudinal && !hasAutoTune) { + modifiedCustomOnroadUIKeys.erase("DeveloperUI"); + } + + toggle->setVisible(modifiedCustomOnroadUIKeys.find(key.c_str()) != modifiedCustomOnroadUIKeys.end()); + } + }); + visualToggle = customUIToggle; + } else if (param == "CustomPaths") { + std::vector pathToggles{"AccelerationPath", "AdjacentPath", "BlindSpotPath", "AdjacentPathMetrics"}; + std::vector pathToggleNames{tr("Acceleration"), tr("Adjacent"), tr("Blind Spot"), tr("Metrics")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, pathToggles, pathToggleNames); + } else if (param == "PedalsOnUI") { + std::vector pedalsToggles{"DynamicPedalsOnUI", "StaticPedalsOnUI"}; + std::vector pedalsToggleNames{tr("Dynamic"), tr("Static")}; + FrogPilotParamToggleControl *pedalsToggle = new FrogPilotParamToggleControl(param, title, desc, icon, pedalsToggles, pedalsToggleNames); + QObject::connect(pedalsToggle, &FrogPilotParamToggleControl::buttonTypeClicked, this, [this, pedalsToggle](int index) { + if (index == 0) { + params.putBool("StaticPedalsOnUI", false); + } else if (index == 1) { + params.putBool("DynamicPedalsOnUI", false); + } + + pedalsToggle->updateButtonStates(); + }); + visualToggle = pedalsToggle; + + } else if (param == "WheelIcon") { + std::vector wheelToggles{"RotatingWheel"}; + std::vector wheelToggleNames{"Live Rotation"}; + std::map steeringWheelLabels = {{-1, tr("None")}, {0, tr("Stock")}, {1, tr("Lexus")}, {2, tr("Toyota")}, {3, tr("Frog")}, {4, tr("Rocket")}, {5, tr("Hyundai")}, {6, tr("Stalin")}}; + visualToggle = new FrogPilotParamValueToggleControl(param, title, desc, icon, -1, 6, steeringWheelLabels, this, true, "", 1, 1, wheelToggles, wheelToggleNames); + + } else if (param == "DeveloperUI") { + FrogPilotParamManageControl *developerUIToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(developerUIToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedDeveloperUIKeys = developerUIKeys ; + + toggle->setVisible(modifiedDeveloperUIKeys.find(key.c_str()) != modifiedDeveloperUIKeys.end()); + } + }); + visualToggle = developerUIToggle; + } else if (param == "BorderMetrics") { + std::vector borderToggles{"BlindSpotMetrics", "ShowSteering", "SignalMetrics"}; + std::vector borderToggleNames{tr("Blind Spot"), tr("Steering Torque"), tr("Turn Signal")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, borderToggles, borderToggleNames); + } else if (param == "LateralMetrics") { + std::vector lateralToggles{"TuningInfo"}; + std::vector lateralToggleNames{tr("Auto Tune")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, lateralToggles, lateralToggleNames); + } else if (param == "LongitudinalMetrics") { + std::vector longitudinalToggles{"JerkInfo", "LeadInfo"}; + std::vector longitudinalToggleNames{tr("Longitudinal Jerk"), tr("Lead Info")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, longitudinalToggles, longitudinalToggleNames); + } else if (param == "NumericalTemp") { + std::vector temperatureToggles{"Fahrenheit"}; + std::vector temperatureToggleNames{tr("Fahrenheit")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, temperatureToggles, temperatureToggleNames); + } else if (param == "SidebarMetrics") { + std::vector sidebarMetricsToggles{"ShowCPU", "ShowGPU", "ShowIP", "ShowMemoryUsage", "ShowStorageLeft", "ShowStorageUsed"}; + std::vector sidebarMetricsToggleNames{tr("CPU"), tr("GPU"), tr("IP"), tr("RAM"), tr("SSD Left"), tr("SSD Used")}; + FrogPilotParamToggleControl *sidebarMetricsToggle = new FrogPilotParamToggleControl(param, title, desc, icon, sidebarMetricsToggles, sidebarMetricsToggleNames, this, 125); + QObject::connect(sidebarMetricsToggle, &FrogPilotParamToggleControl::buttonTypeClicked, this, [this, sidebarMetricsToggle](int index) { + if (index == 0) { + params.putBool("ShowGPU", false); + } else if (index == 1) { + params.putBool("ShowCPU", false); + } else if (index == 3) { + params.putBool("ShowStorageLeft", false); + params.putBool("ShowStorageUsed", false); + } else if (index == 4) { + params.putBool("ShowMemoryUsage", false); + params.putBool("ShowStorageUsed", false); + } else if (index == 5) { + params.putBool("ShowMemoryUsage", false); + params.putBool("ShowStorageLeft", false); + } + + sidebarMetricsToggle->updateButtonStates(); + }); + visualToggle = sidebarMetricsToggle; + + } else if (param == "ModelUI") { + FrogPilotParamManageControl *modelUIToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(modelUIToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + std::set modifiedModelUIKeysKeys = modelUIKeys; + + if (!hasOpenpilotLongitudinal) { + modifiedModelUIKeysKeys.erase("HideLeadMarker"); + } + + toggle->setVisible(modifiedModelUIKeysKeys.find(key.c_str()) != modifiedModelUIKeysKeys.end()); + } + }); + visualToggle = modelUIToggle; + } else if (param == "LaneLinesWidth" || param == "RoadEdgesWidth") { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 24, std::map(), this, false, tr(" inches")); + } else if (param == "PathEdgeWidth") { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 100, std::map(), this, false, tr("%")); + } else if (param == "PathWidth") { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 100, std::map(), this, false, tr(" feet"), 10); + + } else if (param == "QOLVisuals") { + FrogPilotParamManageControl *qolToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(qolToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(qolKeys.find(key.c_str()) != qolKeys.end()); + } + }); + visualToggle = qolToggle; + } else if (param == "CameraView") { + std::vector cameraOptions{tr("Auto"), tr("Driver"), tr("Standard"), tr("Wide")}; + FrogPilotButtonParamControl *preferredCamera = new FrogPilotButtonParamControl(param, title, desc, icon, cameraOptions); + visualToggle = preferredCamera; + } else if (param == "BigMap") { + std::vector mapToggles{"FullMap"}; + std::vector mapToggleNames{tr("Full Map")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, mapToggles, mapToggleNames); + } else if (param == "HideSpeed") { + std::vector hideSpeedToggles{"HideSpeedUI"}; + std::vector hideSpeedToggleNames{tr("Control Via UI")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, hideSpeedToggles, hideSpeedToggleNames); + } else if (param == "MapStyle") { + QMap styleMap = { + {0, tr("Stock openpilot")}, + {1, tr("Mapbox Streets")}, + {2, tr("Mapbox Outdoors")}, + {3, tr("Mapbox Light")}, + {4, tr("Mapbox Dark")}, + {5, tr("Mapbox Satellite")}, + {6, tr("Mapbox Satellite Streets")}, + {7, tr("Mapbox Navigation Day")}, + {8, tr("Mapbox Navigation Night")}, + {9, tr("Mapbox Traffic Night")}, + {10, tr("mike854's (Satellite hybrid)")}, + }; + + QStringList styles = styleMap.values(); + ButtonControl *mapStyleButton = new ButtonControl(title, tr("SELECT"), desc); + QObject::connect(mapStyleButton, &ButtonControl::clicked, [=]() { + QStringList styles = styleMap.values(); + QString selection = MultiOptionDialog::getSelection(tr("Select a map style"), styles, "", this); + if (!selection.isEmpty()) { + int selectedStyle = styleMap.key(selection); + params.putIntNonBlocking("MapStyle", selectedStyle); + mapStyleButton->setValue(selection); + updateFrogPilotToggles(); + } + }); + + int currentStyle = params.getInt("MapStyle"); + mapStyleButton->setValue(styleMap[currentStyle]); + + visualToggle = mapStyleButton; + + } else if (param == "ScreenManagement") { + FrogPilotParamManageControl *screenToggle = new FrogPilotParamManageControl(param, title, desc, icon, this); + QObject::connect(screenToggle, &FrogPilotParamManageControl::manageButtonClicked, this, [this]() { + openParentToggle(); + for (auto &[key, toggle] : toggles) { + toggle->setVisible(screenKeys.find(key.c_str()) != screenKeys.end()); + } + }); + visualToggle = screenToggle; + } else if (param == "HideUIElements") { + std::vector uiElementsToggles{"HideAlerts", "HideMapIcon", "HideMaxSpeed"}; + std::vector uiElementsToggleNames{tr("Alerts"), tr("Map Icon"), tr("Max Speed")}; + visualToggle = new FrogPilotParamToggleControl(param, title, desc, icon, uiElementsToggles, uiElementsToggleNames); + } else if (param == "ScreenBrightness" || param == "ScreenBrightnessOnroad") { + std::map brightnessLabels; + if (param == "ScreenBrightnessOnroad") { + for (int i = 0; i <= 101; i++) { + brightnessLabels[i] = (i == 0) ? tr("Screen Off") : (i == 101) ? tr("Auto") : QString::number(i) + "%"; + } + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 101, brightnessLabels, this, false); + } else { + for (int i = 1; i <= 101; i++) { + brightnessLabels[i] = (i == 101) ? tr("Auto") : QString::number(i) + "%"; + } + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 101, brightnessLabels, this, false); + } + } else if (param == "ScreenTimeout" || param == "ScreenTimeoutOnroad") { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 5, 60, std::map(), this, false, tr(" seconds")); + + } else { + visualToggle = new ParamControl(param, title, desc, icon, this); + } + + addItem(visualToggle); + toggles[param.toStdString()] = visualToggle; + + QObject::connect(static_cast(visualToggle), &ToggleControl::toggleFlipped, &updateFrogPilotToggles); + QObject::connect(static_cast(visualToggle), &FrogPilotParamToggleControl::buttonTypeClicked, &updateFrogPilotToggles); + QObject::connect(static_cast(visualToggle), &FrogPilotParamValueControl::valueChanged, [this]() { + bool screen_management = params.getBool("ScreenManagement"); + if (!started) { + uiState()->scene.screen_brightness = screen_management ? params.getInt("ScreenBrightness") : 101; + } else { + uiState()->scene.screen_brightness_onroad = screen_management ? params.getInt("ScreenBrightnessOnroad") : 101; + } + updateFrogPilotToggles(); + }); + + QObject::connect(visualToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + + QObject::connect(static_cast(visualToggle), &FrogPilotParamManageControl::manageButtonClicked, [this]() { + update(); + }); + } + + QObject::connect(parent, &SettingsWindow::closeParentToggle, this, &FrogPilotVisualsPanel::hideToggles); + QObject::connect(parent, &SettingsWindow::updateMetric, this, &FrogPilotVisualsPanel::updateMetric); + QObject::connect(uiState(), &UIState::offroadTransition, this, &FrogPilotVisualsPanel::updateCarToggles); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotVisualsPanel::updateState); + + updateMetric(); +} + +void FrogPilotVisualsPanel::showEvent(QShowEvent *event) { + hasOpenpilotLongitudinal &= !params.getBool("DisableOpenpilotLongitudinal"); +} + +void FrogPilotVisualsPanel::updateState(const UIState &s) { + if (!isVisible()) return; + + started = s.scene.started; +} + +void FrogPilotVisualsPanel::updateCarToggles() { + auto carParams = params.get("CarParamsPersistent"); + if (!carParams.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(carParams.data(), carParams.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + auto carName = CP.getCarName(); + + hasAutoTune = (carName == "hyundai" || carName == "toyota") && CP.getLateralTuning().which() == cereal::CarParams::LateralTuning::TORQUE; + hasBSM = CP.getEnableBsm(); + hasOpenpilotLongitudinal = CP.getOpenpilotLongitudinalControl() && !params.getBool("DisableOpenpilotLongitudinal"); + } else { + hasAutoTune = true; + hasBSM = true; + hasOpenpilotLongitudinal = true; + } + + hideToggles(); +} + +void FrogPilotVisualsPanel::updateMetric() { + bool previousIsMetric = isMetric; + isMetric = params.getBool("IsMetric"); + + if (isMetric != previousIsMetric) { + double distanceConversion = isMetric ? INCH_TO_CM : CM_TO_INCH; + double speedConversion = isMetric ? FOOT_TO_METER : METER_TO_FOOT; + + params.putIntNonBlocking("LaneLinesWidth", std::nearbyint(params.getInt("LaneLinesWidth") * distanceConversion)); + params.putIntNonBlocking("RoadEdgesWidth", std::nearbyint(params.getInt("RoadEdgesWidth") * distanceConversion)); + + params.putIntNonBlocking("PathWidth", std::nearbyint(params.getInt("PathWidth") * speedConversion)); + } + + FrogPilotParamValueControl *laneLinesWidthToggle = static_cast(toggles["LaneLinesWidth"]); + FrogPilotParamValueControl *roadEdgesWidthToggle = static_cast(toggles["RoadEdgesWidth"]); + FrogPilotParamValueControl *pathWidthToggle = static_cast(toggles["PathWidth"]); + + if (isMetric) { + laneLinesWidthToggle->setDescription(tr("Customize the lane line width.\n\nDefault matches the Vienna average of 10 centimeters.")); + roadEdgesWidthToggle->setDescription(tr("Customize the road edges width.\n\nDefault is 1/2 of the Vienna average lane line width of 10 centimeters.")); + + laneLinesWidthToggle->updateControl(0, 60, tr(" centimeters")); + roadEdgesWidthToggle->updateControl(0, 60, tr(" centimeters")); + + pathWidthToggle->updateControl(0, 30, tr(" meters"), 10); + } else { + laneLinesWidthToggle->setDescription(tr("Customize the lane line width.\n\nDefault matches the MUTCD average of 4 inches.")); + roadEdgesWidthToggle->setDescription(tr("Customize the road edges width.\n\nDefault is 1/2 of the MUTCD average lane line width of 4 inches.")); + + laneLinesWidthToggle->updateControl(0, 24, tr(" inches")); + roadEdgesWidthToggle->updateControl(0, 24, tr(" inches")); + + pathWidthToggle->updateControl(0, 100, tr(" feet"), 10); + } + + laneLinesWidthToggle->refresh(); + roadEdgesWidthToggle->refresh(); +} + +void FrogPilotVisualsPanel::hideToggles() { + for (auto &[key, toggle] : toggles) { + bool subToggles = alertVolumeControlKeys.find(key.c_str()) != alertVolumeControlKeys.end() || + customAlertsKeys.find(key.c_str()) != customAlertsKeys.end() || + customOnroadUIKeys.find(key.c_str()) != customOnroadUIKeys.end() || + customThemeKeys.find(key.c_str()) != customThemeKeys.end() || + developerUIKeys.find(key.c_str()) != developerUIKeys.end() || + modelUIKeys.find(key.c_str()) != modelUIKeys.end() || + qolKeys.find(key.c_str()) != qolKeys.end() || + screenKeys.find(key.c_str()) != screenKeys.end(); + toggle->setVisible(!subToggles); + } + + update(); +} diff --git a/selfdrive/frogpilot/ui/qt/offroad/visual_settings.h b/selfdrive/frogpilot/ui/qt/offroad/visual_settings.h new file mode 100644 index 0000000000..57183e41a2 --- /dev/null +++ b/selfdrive/frogpilot/ui/qt/offroad/visual_settings.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "selfdrive/ui/qt/offroad/settings.h" +#include "selfdrive/ui/ui.h" + +class FrogPilotVisualsPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotVisualsPanel(SettingsWindow *parent); + +signals: + void openParentToggle(); + +private: + void hideToggles(); + void showEvent(QShowEvent *event); + void updateCarToggles(); + void updateMetric(); + void updateState(const UIState &s); + + std::set alertVolumeControlKeys = {"DisengageVolume", "EngageVolume", "PromptDistractedVolume", "PromptVolume", "RefuseVolume", "WarningImmediateVolume", "WarningSoftVolume"}; + std::set customAlertsKeys = {"GreenLightAlert", "LeadDepartingAlert", "LoudBlindspotAlert"}; + std::set customOnroadUIKeys = {"Compass", "CustomPaths", "PedalsOnUI", "RoadNameUI", "ShowStoppingPoint", "WheelIcon"}; + std::set customThemeKeys = {"CustomColors", "CustomIcons", "CustomSignals", "CustomSounds", "HolidayThemes", "RandomEvents"}; + std::set developerUIKeys = {"BorderMetrics", "FPSCounter", "LateralMetrics", "LongitudinalMetrics", "NumericalTemp", "SidebarMetrics", "UseSI"}; + std::set modelUIKeys = {"DynamicPathWidth", "HideLeadMarker", "LaneLinesWidth", "PathEdgeWidth", "PathWidth", "RoadEdgesWidth", "UnlimitedLength"}; + std::set qolKeys = {"BigMap", "CameraView", "DriverCamera", "FullMap", "HideSpeed", "MapStyle", "StoppedTimer", "WheelSpeed"}; + std::set screenKeys = {"HideUIElements", "ScreenBrightness", "ScreenBrightnessOnroad", "ScreenRecorder", "ScreenTimeout", "ScreenTimeoutOnroad", "StandbyMode"}; + + std::map toggles; + + Params params; + + bool hasAutoTune; + bool hasBSM; + bool hasOpenpilotLongitudinal; + bool isMetric = params.getBool("IsMetric"); + bool isRelease; + bool started; +}; diff --git a/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.cc b/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.cc index 771e6c8c6a..fef19f0427 100644 --- a/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.cc +++ b/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.cc @@ -9,3 +9,41 @@ void updateFrogPilotToggles() { paramsMemory.putBool("FrogPilotTogglesUpdated", false); }).detach(); } + +bool FrogPilotConfirmationDialog::toggle(const QString &prompt_text, const QString &confirm_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, confirm_text, tr("Reboot Later"), false, parent); + return d.exec(); +} + +bool FrogPilotConfirmationDialog::toggleAlert(const QString &prompt_text, const QString &button_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, button_text, "", false, parent); + return d.exec(); +} + +bool FrogPilotConfirmationDialog::yesorno(const QString &prompt_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Yes"), tr("No"), false, parent); + return d.exec(); +} + +FrogPilotButtonIconControl::FrogPilotButtonIconControl(const QString &title, const QString &text, const QString &desc, const QString &icon, QWidget *parent) : AbstractControl(title, desc, icon, parent) { + btn.setText(text); + btn.setStyleSheet(R"( + QPushButton { + padding: 0; + border-radius: 50px; + font-size: 35px; + font-weight: 500; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"); + btn.setFixedSize(250, 100); + QObject::connect(&btn, &QPushButton::clicked, this, &FrogPilotButtonIconControl::clicked); + hlayout->addWidget(&btn); +} diff --git a/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.h b/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.h index 3364551509..a1e1011211 100644 --- a/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.h +++ b/selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.h @@ -1,3 +1,708 @@ #pragma once +#include + +#include + +#include "selfdrive/ui/qt/widgets/controls.h" + void updateFrogPilotToggles(); + +class FrogPilotConfirmationDialog : public ConfirmationDialog { + Q_OBJECT + +public: + explicit FrogPilotConfirmationDialog(const QString &prompt_text, const QString &confirm_text, + const QString &cancel_text, const bool rich, QWidget *parent); + static bool toggle(const QString &prompt_text, const QString &confirm_text, QWidget *parent); + static bool toggleAlert(const QString &prompt_text, const QString &button_text, QWidget *parent); + static bool yesorno(const QString &prompt_text, QWidget *parent); +}; + +class FrogPilotListWidget : public QWidget { + Q_OBJECT +public: + explicit FrogPilotListWidget(QWidget *parent = nullptr) : QWidget(parent), outer_layout(this) { + outer_layout.setMargin(0); + outer_layout.setSpacing(0); + outer_layout.addLayout(&inner_layout); + inner_layout.setMargin(0); + inner_layout.setSpacing(25); // default spacing is 25 + } + + inline void addItem(QWidget *w) { + inner_layout.addWidget(w); + adjustStretch(); + } + + inline void addItem(QLayout *layout) { + inner_layout.addLayout(layout); + adjustStretch(); + } + + inline void setSpacing(int spacing) { + inner_layout.setSpacing(spacing); + adjustStretch(); + } + +private: + void adjustStretch() { + if (inner_layout.stretch(inner_layout.count() - 1) > 0) { + inner_layout.setStretch(inner_layout.count() - 1, 0); + } + if (inner_layout.count() > 3) { + outer_layout.addStretch(); + } + } + + void paintEvent(QPaintEvent *event) override { + QPainter p(this); + p.setPen(Qt::gray); + + int visibleWidgetCount = 0; + std::vector visibleRects; + + for (int i = 0; i < inner_layout.count(); ++i) { + QWidget *widget = inner_layout.itemAt(i)->widget(); + if (widget && widget->isVisible()) { + visibleWidgetCount++; + visibleRects.push_back(inner_layout.itemAt(i)->geometry()); + } + } + + for (int i = 0; i < visibleWidgetCount - 1; ++i) { + int bottom = visibleRects[i].bottom() + inner_layout.spacing() / 2; + p.drawLine(visibleRects[i].left() + 40, bottom, visibleRects[i].right() - 40, bottom); + } + } + + QVBoxLayout outer_layout; + QVBoxLayout inner_layout; +}; + +class FrogPilotDualParamControl : public QFrame { + Q_OBJECT + +public: + FrogPilotDualParamControl(ParamControl *control1, ParamControl *control2, QWidget *parent = nullptr, bool split=false) + : QFrame(parent) { + QHBoxLayout *hlayout = new QHBoxLayout(this); + + control1->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + control1->setMaximumWidth(split ? 850 : 700); + + control2->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + control2->setMaximumWidth(split ? 700 : 850); + + hlayout->addWidget(control1); + hlayout->addWidget(control2); + } +}; + +class FrogPilotButtonControl : public AbstractControl { + Q_OBJECT + +public: + FrogPilotButtonControl(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); + inline void setText(const QString &text) { btn.setText(text); } + inline QString text() const { return btn.text(); } + +signals: + void clicked(); + +public slots: + void setEnabled(bool enabled) { btn.setEnabled(enabled); } + +private: + QPushButton btn; +}; + +class FrogPilotButtonIconControl : public AbstractControl { + Q_OBJECT + +public: + FrogPilotButtonIconControl(const QString &title, const QString &text, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + inline void setText(const QString &text) { btn.setText(text); } + inline QString text() const { return btn.text(); } + +signals: + void clicked(); + +public slots: + void setEnabled(bool enabled) { btn.setEnabled(enabled); } + +private: + QPushButton btn; +}; + +class FrogPilotButtonParamControl : public ParamControl { + Q_OBJECT +public: + FrogPilotButtonParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, const int minimum_button_width = 225) + : ParamControl(param, title, desc, icon) { + const QString style = R"( + QPushButton { + border-radius: 50px; + font-size: 40px; + font-weight: 500; + height:100px; + padding: 0 25 0 25; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + + key = param.toStdString(); + int value = atoi(params.get(key).c_str()); + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + for (size_t i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(true); + button->setChecked(i == value); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + hlayout->addWidget(button); + button_group->addButton(button, i); + } + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonToggled), [=](int id, bool checked) { + if (checked) { + params.put(key, std::to_string(id)); + refresh(); + emit buttonClicked(id); + } + }); + + toggle.hide(); + } + + void setEnabled(bool enable) { + for (auto btn : button_group->buttons()) { + btn->setEnabled(enable); + } + } + +signals: + void buttonClicked(int id); + +private: + std::string key; + Params params; + QButtonGroup *button_group; +}; + +class FrogPilotButtonsControl : public ParamControl { + Q_OBJECT +public: + FrogPilotButtonsControl(const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, const int minimum_button_width = 225) + : ParamControl("", title, desc, icon) { + const QString style = R"( + QPushButton { + border-radius: 50px; + font-size: 40px; + font-weight: 500; + height: 100px; + padding: 0 25px 0 25px; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + + button_group = new QButtonGroup(this); + + for (size_t i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + hlayout->addWidget(button); + button_group->addButton(button, static_cast(i)); + + connect(button, &QPushButton::clicked, this, [this, i]() { + emit buttonClicked(static_cast(i)); + }); + } + + toggle.hide(); + } + +signals: + void buttonClicked(int id); + +private: + QButtonGroup *button_group; +}; + +class FrogPilotButtonsParamControl : public ParamControl { + Q_OBJECT +public: + FrogPilotButtonsParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector> &button_params) + : ParamControl(param, title, desc, icon) { + const QString style = R"( + QPushButton { + border-radius: 50px; + font-size: 40px; + font-weight: 500; + height:100px; + padding: 0 25 0 25; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + + for (const auto ¶m_pair : button_params) { + const QString ¶m_toggle = param_pair.first; + const QString &button_text = param_pair.second; + + QPushButton *button = new QPushButton(button_text, this); + button->setCheckable(true); + + bool value = params.getBool(param_toggle.toStdString()); + button->setChecked(value); + button->setStyleSheet(style); + button->setMinimumWidth(225); + hlayout->addWidget(button); + + QObject::connect(button, &QPushButton::toggled, this, [=](bool checked) { + if (checked) { + for (const auto &inner_param_pair : button_params) { + const QString &inner_param = inner_param_pair.first; + params.putBool(inner_param.toStdString(), inner_param == param_toggle); + } + refresh(); + emit buttonClicked(); + } + }); + + button_group->addButton(button); + } + + toggle.hide(); + } + + void setEnabled(bool enable) { + for (auto btn : button_group->buttons()) { + btn->setEnabled(enable); + } + } + +signals: + void buttonClicked(); + +private: + Params params; + QButtonGroup *button_group; +}; + +class FrogPilotParamManageControl : public ParamControl { + Q_OBJECT + +public: + FrogPilotParamManageControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr, bool hideToggle = false) + : ParamControl(param, title, desc, icon, parent), + hideToggle(hideToggle), + key(param.toStdString()), + manageButton(new ButtonControl(tr(""), tr("MANAGE"), tr(""))) { + hlayout->insertWidget(hlayout->indexOf(&toggle) - 1, manageButton); + + connect(this, &ToggleControl::toggleFlipped, this, [this](bool state) { + refresh(); + }); + + connect(manageButton, &ButtonControl::clicked, this, &FrogPilotParamManageControl::manageButtonClicked); + + if (hideToggle) { + toggle.hide(); + } + } + + void refresh() { + ParamControl::refresh(); + manageButton->setVisible(params.getBool(key) || hideToggle); + } + + void setEnabled(bool enabled) { + manageButton->setEnabled(enabled); + toggle.setEnabled(enabled); + toggle.update(); + } + + void showEvent(QShowEvent *event) override { + ParamControl::showEvent(event); + refresh(); + } + +signals: + void manageButtonClicked(); + +private: + bool hideToggle; + std::string key; + Params params; + ButtonControl *manageButton; +}; + +class FrogPilotParamToggleControl : public ParamControl { + Q_OBJECT +public: + FrogPilotParamToggleControl(const QString ¶m, const QString &title, const QString &desc, + const QString &icon, const std::vector &button_params, + const std::vector &button_texts, QWidget *parent = nullptr, + const int minimum_button_width = 225) + : ParamControl(param, title, desc, icon, parent) { + + key = param.toStdString(); + + connect(this, &ToggleControl::toggleFlipped, this, [this](bool state) { + refreshButtons(state); + }); + + const QString style = R"( + QPushButton { + border-radius: 50px; + font-size: 40px; + font-weight: 500; + height:100px; + padding: 0 25 0 25; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + + button_group = new QButtonGroup(this); + button_group->setExclusive(false); + this->button_params = button_params; + + for (int i = 0; i < button_texts.size(); ++i) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(true); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + button_group->addButton(button, i); + + connect(button, &QPushButton::clicked, [this, i](bool checked) { + params.putBool(this->button_params[i].toStdString(), checked); + button_group->button(i)->setChecked(checked); + emit buttonClicked(checked); + emit buttonTypeClicked(i); + }); + + hlayout->insertWidget(hlayout->indexOf(&toggle) - 1, button); + } + } + + void refresh() { + bool state = params.getBool(key); + if (state != toggle.on) { + toggle.togglePosition(); + } + + refreshButtons(state); + updateButtonStates(); + } + + void refreshButtons(bool state) { + for (QAbstractButton *button : button_group->buttons()) { + button->setVisible(state); + } + } + + void updateButtonStates() { + for (int i = 0; i < button_group->buttons().size(); ++i) { + bool checked = params.getBool(button_params[i].toStdString()); + QAbstractButton *button = button_group->button(i); + if (button) { + button->setChecked(checked); + } + } + } + + void showEvent(QShowEvent *event) override { + refresh(); + QWidget::showEvent(event); + } + +signals: + void buttonClicked(const bool checked); + void buttonTypeClicked(int i); + +private: + std::string key; + Params params; + QButtonGroup *button_group; + std::vector button_params; +}; + +class FrogPilotParamValueControl : public ParamControl { + Q_OBJECT + +public: + FrogPilotParamValueControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const float &minValue, const float &maxValue, const std::map &valueLabels, + QWidget *parent = nullptr, const bool &loop = true, const QString &label = "", + const float &division = 1.0f, const float &interval = 1.0f) + : ParamControl(param, title, desc, icon, parent), + minValue(minValue), maxValue(maxValue), valueLabelMappings(valueLabels), loop(loop), labelText(label), + division(division), interval(interval), previousValue(0.0f), value(0.0f) { + key = param.toStdString(); + + valueLabel = new QLabel(this); + hlayout->addWidget(valueLabel); + + QPushButton *decrementButton = createButton("-", this); + QPushButton *incrementButton = createButton("+", this); + + hlayout->addWidget(decrementButton); + hlayout->addWidget(incrementButton); + + countdownTimer = new QTimer(this); + countdownTimer->setInterval(150); + countdownTimer->setSingleShot(true); + + connect(countdownTimer, &QTimer::timeout, this, &FrogPilotParamValueControl::handleTimeout); + + connect(decrementButton, &QPushButton::pressed, this, [=]() { updateValue(-interval); }); + connect(incrementButton, &QPushButton::pressed, this, [=]() { updateValue(interval); }); + + connect(decrementButton, &QPushButton::released, this, &FrogPilotParamValueControl::restartTimer); + connect(incrementButton, &QPushButton::released, this, &FrogPilotParamValueControl::restartTimer); + + toggle.hide(); + } + + void restartTimer() { + countdownTimer->stop(); + countdownTimer->start(); + + emit valueChanged(value); + } + + void handleTimeout() { + previousValue = value; + } + + void updateValue(float intervalChange) { + int previousValueAdjusted = round(previousValue * 100) / 100 / intervalChange; + int valueAdjusted = round(value * 100) / 100 / intervalChange; + + if (std::fabs(previousValueAdjusted - valueAdjusted) > 5 && std::fmod(valueAdjusted, 5) == 0) { + intervalChange *= 5; + } + + value += intervalChange; + + if (loop) { + if (value < minValue) { + value = maxValue; + } else if (value > maxValue) { + value = minValue; + } + } else { + value = std::max(minValue, std::min(maxValue, value)); + } + + params.putFloat(key, value); + refresh(); + } + + void refresh() { + value = params.getFloat(key); + + QString text; + auto it = valueLabelMappings.find(value); + int decimals = interval < 1.0f ? static_cast(-std::log10(interval)) : 2; + + if (division > 1.0f) { + text = QString::number(value / division, 'g', division >= 10.0f ? 4 : 3); + } else { + if (it != valueLabelMappings.end()) { + text = it->second; + } else { + if (value >= 100.0f) { + text = QString::number(value, 'f', 0); + } else { + text = QString::number(value, interval < 1.0f ? 'f' : 'g', decimals); + } + } + } + + if (!labelText.isEmpty()) { + text += labelText; + } + + valueLabel->setText(text); + valueLabel->setStyleSheet("QLabel { color: #E0E879; }"); + } + + void updateControl(float newMinValue, float newMaxValue, const QString &newLabel, float newDivision = 1.0f) { + minValue = newMinValue; + maxValue = newMaxValue; + labelText = newLabel; + division = newDivision; + } + + void showEvent(QShowEvent *event) override { + refresh(); + previousValue = value; + } + +signals: + void valueChanged(float value); + +private: + Params params; + + bool loop; + + float division; + float interval; + float maxValue; + float minValue; + float previousValue; + float value; + + QLabel *valueLabel; + QString labelText; + + std::map valueLabelMappings; + std::string key; + + QTimer *countdownTimer; + + QPushButton *createButton(const QString &text, QWidget *parent) { + QPushButton *button = new QPushButton(text, parent); + button->setFixedSize(150, 100); + button->setAutoRepeat(true); + button->setAutoRepeatInterval(150); + button->setAutoRepeatDelay(500); + button->setStyleSheet(R"( + QPushButton { + border-radius: 50px; + font-size: 50px; + font-weight: 500; + height: 100px; + padding: 0 25 0 25; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + )"); + return button; + } +}; + +class FrogPilotParamValueToggleControl : public FrogPilotParamValueControl { + Q_OBJECT + +public: + FrogPilotParamValueToggleControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const float &minValue, const float &maxValue, const std::map &valueLabels, + QWidget *parent = nullptr, const bool &loop = true, const QString &label = "", + const float &division = 1.0f, const float &interval = 1.0f, + const std::vector &button_params = std::vector(), const std::vector &button_texts = std::vector(), + const int minimum_button_width = 225) + : FrogPilotParamValueControl(param, title, desc, icon, minValue, maxValue, valueLabels, parent, loop, label, division, interval) { + + const QString style = R"( + QPushButton { + border-radius: 50px; + font-size: 40px; + font-weight: 500; + height: 100px; + padding: 0 25 0 25; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + + button_group = new QButtonGroup(this); + button_group->setExclusive(false); + + for (int i = 0; i < button_texts.size(); ++i) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(true); + button->setChecked(params.getBool(button_params[i].toStdString())); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + button_group->addButton(button, i); + + connect(button, &QPushButton::clicked, [this, button_params, i](bool checked) { + params.putBool(button_params[i].toStdString(), checked); + emit buttonClicked(); + refresh(); + }); + + buttons[button_params[i]] = button; + hlayout->insertWidget(3, button); + } + } + + void refresh() { + FrogPilotParamValueControl::refresh(); + + auto keys = buttons.keys(); + for (const QString ¶m : keys) { + QPushButton *button = buttons.value(param); + button->setChecked(params.getBool(param.toStdString())); + } + } + +signals: + void buttonClicked(); + +private: + Params params; + QButtonGroup *button_group; + QMap buttons; +}; diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index e181cb9abd..29e1638429 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -22,7 +22,10 @@ widgets_src = ["ui.cc", "qt/widgets/input.cc", "qt/widgets/wifi.cc", "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", - "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc"] + "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc", + "../frogpilot/ui/qt/widgets/frogpilot_controls.cc", + "../frogpilot/ui/qt/offroad/control_settings.cc", "../frogpilot/ui/qt/offroad/vehicle_settings.cc", + "../frogpilot/ui/qt/offroad/visual_settings.cc"] qt_env['CPPDEFINES'] = [] if maps: diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 5aa33974ac..063ae4817f 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -5,6 +5,7 @@ #include #include +#include #include "common/watchdog.h" #include "common/util.h" @@ -15,6 +16,10 @@ #include "selfdrive/ui/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/widgets/ssh_keys.h" +#include "selfdrive/frogpilot/ui/qt/offroad/control_settings.h" +#include "selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.h" +#include "selfdrive/frogpilot/ui/qt/offroad/visual_settings.h" + TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { // param, title, desc, icon std::vector> toggle_defs{ @@ -114,6 +119,11 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { connect(toggles["ExperimentalLongitudinalEnabled"], &ToggleControl::toggleFlipped, [=]() { updateToggles(); }); + + // FrogPilot signals + connect(toggles["IsMetric"], &ToggleControl::toggleFlipped, [=]() { + updateMetric(); + }); } void TogglesPanel::updateState(const UIState &s) { @@ -348,6 +358,16 @@ void DevicePanel::showEvent(QShowEvent *event) { ListWidget::showEvent(event); } +void SettingsWindow::hideEvent(QHideEvent *event) { + closeParentToggle(); + + parentToggleOpen = false; + subParentToggleOpen = false; + subSubParentToggleOpen = false; + + previousScrollPosition = 0; +} + void SettingsWindow::showEvent(QShowEvent *event) { setCurrentPanel(0); } @@ -384,7 +404,20 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { close_btn->setFixedSize(200, 200); sidebar_layout->addSpacing(45); sidebar_layout->addWidget(close_btn, 0, Qt::AlignCenter); - QObject::connect(close_btn, &QPushButton::clicked, this, &SettingsWindow::closeSettings); + QObject::connect(close_btn, &QPushButton::clicked, [this]() { + if (subSubParentToggleOpen) { + closeSubSubParentToggle(); + subSubParentToggleOpen = false; + } else if (subParentToggleOpen) { + closeSubParentToggle(); + subParentToggleOpen = false; + } else if (parentToggleOpen) { + closeParentToggle(); + parentToggleOpen = false; + } else { + closeSettings(); + } + }); // setup panels DevicePanel *device = new DevicePanel(this); @@ -393,12 +426,24 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { TogglesPanel *toggles = new TogglesPanel(this); QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + QObject::connect(toggles, &TogglesPanel::updateMetric, this, &SettingsWindow::updateMetric); + + FrogPilotControlsPanel *frogpilotControls = new FrogPilotControlsPanel(this); + QObject::connect(frogpilotControls, &FrogPilotControlsPanel::openParentToggle, this, [this]() {parentToggleOpen=true;}); + QObject::connect(frogpilotControls, &FrogPilotControlsPanel::openSubParentToggle, this, [this]() {subParentToggleOpen=true;}); + QObject::connect(frogpilotControls, &FrogPilotControlsPanel::openSubSubParentToggle, this, [this]() {subSubParentToggleOpen=true;}); + + FrogPilotVisualsPanel *frogpilotVisuals = new FrogPilotVisualsPanel(this); + QObject::connect(frogpilotVisuals, &FrogPilotVisualsPanel::openParentToggle, this, [this]() {parentToggleOpen=true;}); QList> panels = { {tr("Device"), device}, {tr("Network"), new Networking(this)}, {tr("Toggles"), toggles}, {tr("Software"), new SoftwarePanel(this)}, + {tr("Controls"), frogpilotControls}, + {tr("Vehicles"), new FrogPilotVehiclesPanel(this)}, + {tr("Visuals"), frogpilotVisuals}, }; nav_btns = new QButtonGroup(this); @@ -431,7 +476,25 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { ScrollView *panel_frame = new ScrollView(panel, this); panel_widget->addWidget(panel_frame); + if (name == tr("Controls") || name == tr("Visuals")) { + QScrollBar *scrollbar = panel_frame->verticalScrollBar(); + + QObject::connect(scrollbar, &QScrollBar::valueChanged, this, [this](int value) { + if (!parentToggleOpen) { + previousScrollPosition = value; + } + }); + + QObject::connect(scrollbar, &QScrollBar::rangeChanged, this, [this, panel_frame]() { + if (!parentToggleOpen) { + panel_frame->restorePosition(previousScrollPosition); + } + }); + } + QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { + closeParentToggle(); + previousScrollPosition = 0; btn->setChecked(true); panel_widget->setCurrentWidget(w); }); diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 8b1ae8ceee..9b680723f3 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -25,17 +25,33 @@ public: protected: void showEvent(QShowEvent *event) override; + // FrogPilot widgets + void hideEvent(QHideEvent *event) override; + signals: void closeSettings(); void reviewTrainingGuide(); void showDriverView(); void expandToggleDescription(const QString ¶m); + // FrogPilot signals + void closeParentToggle(); + void closeSubParentToggle(); + void closeSubSubParentToggle(); + void updateMetric(); + private: QPushButton *sidebar_alert_widget; QWidget *sidebar_widget; QButtonGroup *nav_btns; QStackedWidget *panel_widget; + + // FrogPilot variables + bool parentToggleOpen; + bool subParentToggleOpen; + bool subSubParentToggleOpen; + + int previousScrollPosition; }; class DevicePanel : public ListWidget { @@ -64,6 +80,10 @@ public: explicit TogglesPanel(SettingsWindow *parent); void showEvent(QShowEvent *event) override; +signals: + // FrogPilot signals + void updateMetric(); + public slots: void expandToggleDescription(const QString ¶m); diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index aa304e0df6..4a2c7f3fdc 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -132,6 +132,10 @@ public: toggle.update(); } + void refresh() { + toggle.togglePosition(); + } + signals: void toggleFlipped(bool state); diff --git a/selfdrive/ui/qt/widgets/scrollview.cc b/selfdrive/ui/qt/widgets/scrollview.cc index 978bf83a63..28460078af 100644 --- a/selfdrive/ui/qt/widgets/scrollview.cc +++ b/selfdrive/ui/qt/widgets/scrollview.cc @@ -44,6 +44,10 @@ ScrollView::ScrollView(QWidget *w, QWidget *parent) : QScrollArea(parent) { scroller->setScrollerProperties(sp); } +void ScrollView::restorePosition(int previousScrollPosition) { + verticalScrollBar()->setValue(previousScrollPosition); +} + void ScrollView::hideEvent(QHideEvent *e) { verticalScrollBar()->setValue(0); } diff --git a/selfdrive/ui/qt/widgets/scrollview.h b/selfdrive/ui/qt/widgets/scrollview.h index 024331aa39..51acc4c432 100644 --- a/selfdrive/ui/qt/widgets/scrollview.h +++ b/selfdrive/ui/qt/widgets/scrollview.h @@ -7,6 +7,10 @@ class ScrollView : public QScrollArea { public: explicit ScrollView(QWidget *w = nullptr, QWidget *parent = nullptr); + + // FrogPilot functions + void restorePosition(int previousScrollPosition); + protected: void hideEvent(QHideEvent *e) override; }; diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 4804e4441f..6c270a098d 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -216,6 +216,7 @@ static void update_state(UIState *s) { } if (sm.updated("deviceState")) { auto deviceState = sm["deviceState"].getDeviceState(); + scene.online = deviceState.getNetworkType() != cereal::DeviceState::NetworkType::NONE; } if (sm.updated("frogpilotCarControl")) { auto frogpilotCarControl = sm["frogpilotCarControl"].getFrogpilotCarControl(); diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index ce90f30443..b513fa7c14 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -116,6 +116,7 @@ typedef struct UIScene { bool enabled; bool experimental_mode; bool map_open; + bool online; bool right_hand_drive; int alert_size;