#include #include #include #include #include #include #include #include #include #include #include "common/watchdog.h" #include "common/util.h" #include "system/hardware/hw.h" #include "selfdrive/ui/qt/network/networking.h" #include "selfdrive/ui/qt/offroad/settings.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/widgets/input.h" #include "selfdrive/ui/qt/widgets/prime.h" #include "selfdrive/ui/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/offroad/developer_panel.h" #include "starpilot/ui/qt/offroad/starpilot_settings.h" TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { syncRhdToggle(); // param, title, desc, icon, restart needed std::vector> toggle_defs{ { "OpenpilotEnabledToggle", tr("Enable openpilot"), tr("Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature."), "../assets/icons/chffr_wheel.png", true, }, { "ExperimentalMode", tr("Experimental Mode"), "", "../assets/icons/experimental_white.svg", false, }, { "SafeMode", tr("Safe Mode"), tr("Temporarily force driving-affecting StarPilot settings back to safe defaults, stock tuning, and the branch default model until disabled."), "../assets/icons/warning.png", true, }, { "SimpleMode", tr("Simple Mode"), tr("Use a more stock-like Qt interface by hiding most branch-specific UI, theme, sound, and alert styling. This only changes presentation and does not change driving behavior."), "../assets/icons/settings.png", false, }, { "DisengageOnAccelerator", tr("Disengage on Accelerator Pedal"), tr("When enabled, pressing the accelerator pedal will disengage openpilot."), "../assets/icons/disengage_on_accelerator.svg", false, }, { "IsLdwEnabled", tr("Enable Lane Departure Warnings"), tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."), "../assets/icons/warning.png", false, }, { "AlwaysOnDM", tr("Always-On Driver Monitoring"), tr("Enable driver monitoring even when openpilot is not engaged."), "../assets/icons/monitoring.png", false, }, { "IsRHD", tr("Right Hand Driving"), tr("Use right-hand-drive driver monitoring. This follows the auto-detected side until changed manually."), "../assets/icons/monitoring.png", false, }, { "RecordFront", tr("Record and Upload Driver Camera"), tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), "../assets/icons/monitoring.png", true, }, { "RecordAudio", tr("Record and Upload Microphone Audio"), tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), "../assets/icons/microphone.png", true, }, { "IsMetric", tr("Use Metric System"), tr("Display speed in km/h instead of mph."), "../assets/icons/metric.png", false, }, }; std::vector longi_button_texts{tr("Aggressive"), tr("Standard"), tr("Relaxed")}; long_personality_setting = new ButtonParamControl("LongitudinalPersonality", tr("Driving Personality"), tr("Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " "In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " "your steering wheel distance button."), "../assets/icons/speed_limit.png", longi_button_texts); // set up uiState update for personality setting QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState); for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { auto toggle = new ParamControl(param, title, desc, icon, this); bool locked = params.getBool((param + "Lock").toStdString()); toggle->setEnabled(!locked); if (needs_restart && !locked) { toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on.")); QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) { toggle->setEnabled(!engaged); }); QObject::connect(toggle, &ParamControl::toggleFlipped, [=](bool state) { params.putBool("OnroadCycleRequested", true); }); } if (param == "SimpleMode") { QObject::connect(toggle, &ParamControl::toggleFlipped, this, [this](bool state) { updateToggles(); emit simpleModeChanged(state); }); } if (param == "IsRHD") { QObject::connect(toggle, &ParamControl::toggleFlipped, this, [this](bool state) { params.putBool("IsRHD", state); params.putBool("IsRHDOverride", true); }); } addItem(toggle); toggles[param.toStdString()] = toggle; // insert longitudinal personality after NDOG toggle if (param == "DisengageOnAccelerator") { addItem(long_personality_setting); } } // Toggles with confirmation dialogs toggles["ExperimentalMode"]->setActiveIcon("../assets/icons/experimental.svg"); toggles["ExperimentalMode"]->setConfirmation(true, true); connect(toggles["IsMetric"], &ToggleControl::toggleFlipped, [=](bool isMetric) { updateMetric(isMetric); }); } void TogglesPanel::syncRhdToggle() { if (!params.getBool("IsRHDOverride")) { params.putBool("IsRHD", params.getBool("IsRhdDetected")); } } void TogglesPanel::updateState(const UIState &s) { const SubMaster &sm = *(s.sm); if (sm.updated("selfdriveState")) { auto personality = sm["selfdriveState"].getSelfdriveState().getPersonality(); if (personality != s.scene.personality && s.scene.started && isVisible()) { long_personality_setting->setCheckedButton(static_cast(personality)); } uiState()->scene.personality = personality; } } void TogglesPanel::expandToggleDescription(const QString ¶m) { toggles[param.toStdString()]->showDescription(); } void TogglesPanel::scrollToToggle(const QString ¶m) { if (auto it = toggles.find(param.toStdString()); it != toggles.end()) { auto scroll_area = qobject_cast(parent()->parent()); if (scroll_area) { scroll_area->ensureWidgetVisible(it->second); } } } void TogglesPanel::showEvent(QShowEvent *event) { updateToggles(); } void TogglesPanel::updateToggles() { syncRhdToggle(); const bool showAllToggles = params.getBool("ShowAllToggles"); const bool safe_mode = params.getBool("SafeMode"); const bool simple_mode = params.getBool("SimpleMode"); if (safe_mode) { if (params.getBool("ExperimentalMode")) { params.putBool("ExperimentalMode", false); } if (params.getInt("LongitudinalPersonality") != static_cast(cereal::LongitudinalPersonality::RELAXED)) { params.putInt("LongitudinalPersonality", static_cast(cereal::LongitudinalPersonality::RELAXED)); } } auto experimental_mode_toggle = toggles["ExperimentalMode"]; const QString e2e_description = QString("%1
" "

%2


" "%3
" "

%4


" "%5
") .arg(tr("openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. Experimental features are listed below:")) .arg(tr("End-to-End Longitudinal Control")) .arg(tr("Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " "mistakes should be expected.")) .arg(tr("New Driving Visualization")) .arg(tr("The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.")); const bool is_release = false; auto cp_bytes = params.get("CarParamsPersistent"); if (!cp_bytes.empty()) { AlignedBuffer aligned_buf; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); cereal::CarParams::Reader CP = cmsg.getRoot(); if (hasLongitudinalControl(CP) || showAllToggles) { // normal description and toggle experimental_mode_toggle->setEnabled(!safe_mode); experimental_mode_toggle->setDescription(e2e_description); long_personality_setting->setEnabled(!safe_mode); if (safe_mode) { long_personality_setting->setCheckedButton(static_cast(cereal::LongitudinalPersonality::RELAXED)); } } else { // no long for now experimental_mode_toggle->setEnabled(false); long_personality_setting->setEnabled(false); params.remove("ExperimentalMode"); const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control."); QString long_desc = unavailable + " " + \ tr("openpilot longitudinal control may come in a future update."); if (CP.getAlphaLongitudinalAvailable()) { if (is_release) { long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); } else { long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode."); } } experimental_mode_toggle->setDescription("" + long_desc + "

" + e2e_description); } experimental_mode_toggle->refresh(); } else { experimental_mode_toggle->setDescription(e2e_description); } if (safe_mode) { experimental_mode_toggle->setEnabled(false); long_personality_setting->setEnabled(false); long_personality_setting->setCheckedButton(static_cast(cereal::LongitudinalPersonality::RELAXED)); } StarPilotUIState &fs = *starpilotUIState(); StarPilotUIScene &starpilot_scene = fs.starpilot_scene; QJsonObject &starpilot_toggles = starpilot_scene.starpilot_toggles; auto disengage_on_accelerator_toggle = toggles["DisengageOnAccelerator"]; disengage_on_accelerator_toggle->setVisible(showAllToggles || !starpilot_toggles.value("always_on_lateral").toBool()); auto driver_camera_toggle = toggles["RecordFront"]; driver_camera_toggle->setVisible(showAllToggles || !starpilot_toggles.value("no_logging").toBool()); experimental_mode_toggle->setVisible(showAllToggles || !starpilot_toggles.value("conditional_experimental_mode").toBool()); auto record_audio_toggle = toggles["RecordAudio"]; record_audio_toggle->setVisible(showAllToggles || !starpilot_toggles.value("no_logging").toBool()); toggles["IsRHD"]->refresh(); auto safe_mode_toggle = toggles["SafeMode"]; if (safe_mode_toggle != nullptr) { safe_mode_toggle->setVisible(!simple_mode); } } GalaxyQRPopup::GalaxyQRPopup(const QString &url, QWidget *parent) : DialogBase(parent) { setStyleSheet("GalaxyQRPopup { background-color: #1a1a30; }"); QVBoxLayout *layout = new QVBoxLayout(this); layout->setAlignment(Qt::AlignCenter); layout->setSpacing(30); layout->setContentsMargins(60, 40, 60, 40); auto qr = qrcodegen::QrCode::encodeText(url.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW); const int qr_size = qr.getSize(); const int quiet_zone_modules = 4; const int qr_display_size = 520; const int image_size = qr_size + 2 * quiet_zone_modules; QImage image(image_size, image_size, QImage::Format_RGB32); image.fill(qRgb(255, 255, 255)); for (int y = 0; y < qr_size; ++y) { for (int x = 0; x < qr_size; ++x) { image.setPixel(x + quiet_zone_modules, y + quiet_zone_modules, qr.getModule(x, y) ? qRgb(0, 0, 0) : qRgb(255, 255, 255)); } } QLabel *title = new QLabel(tr("Scan to open Galaxy"), this); title->setStyleSheet("font-size: 52px; font-weight: bold; color: white;"); title->setAlignment(Qt::AlignCenter); layout->addWidget(title); QLabel *qr_label = new QLabel(this); qr_label->setPixmap(QPixmap::fromImage( image.scaled(qr_display_size, qr_display_size, Qt::KeepAspectRatio), Qt::MonoOnly)); qr_label->setAlignment(Qt::AlignCenter); layout->addWidget(qr_label); QLabel *url_label = new QLabel(url, this); url_label->setStyleSheet("font-size: 36px; color: #8b6cc5;"); url_label->setAlignment(Qt::AlignCenter); layout->addWidget(url_label); QLabel *hint = new QLabel(tr("Tap anywhere to dismiss"), this); hint->setStyleSheet("font-size: 28px; color: #7e7e98;"); hint->setAlignment(Qt::AlignCenter); layout->addWidget(hint); } DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { setSpacing(50); addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A")))); addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); pair_device = new ButtonControl(tr("Pair Device"), tr("PAIR"), useKonikServer() ? tr("Pair your device with Konik connect (stable.konik.ai).") : tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); connect(pair_device, &ButtonControl::clicked, [=]() { PairingPopup popup(this); popup.exec(); }); addItem(pair_device); const std::string galaxy_dir = Hardware::PC() ? (Path::comma_home() + "/starpilot/data/galaxy") : "/data/galaxy"; const std::string galaxy_auth_path = galaxy_dir + "/glxyauth"; const std::string galaxy_session_path = galaxy_dir + "/glxysession"; const std::string galaxy_slug_path = galaxy_dir + "/glxyslug"; auto showGalaxyQR = [=]() { std::string slug = util::read_file(galaxy_slug_path); if (slug.empty()) { ConfirmationDialog::alert(tr("Galaxy is not paired yet."), this); return; } GalaxyQRPopup popup("https://galaxy.firestar.link/" + QString::fromStdString(slug), this); popup.exec(); }; pair_galaxy = new ButtonControl(tr("Galaxy"), tr("PAIR"), tr("Pair your device with Galaxy for remote access to The Galaxy.")); connect(pair_galaxy, &ButtonControl::clicked, [=]() { const std::string current_password = util::read_file(galaxy_auth_path); if (current_password.empty()) { QString new_password = InputDialog::getText( tr("Set Galaxy Password"), this, tr("Set a password to secure your Galaxy access. (Min 6 characters)"), false, 6); if (new_password.isEmpty()) { return; } std::string hash = QCryptographicHash::hash(new_password.toUtf8(), QCryptographicHash::Sha256).toHex().toStdString(); util::create_directories(galaxy_dir, 0775); util::write_file(galaxy_auth_path.c_str(), hash.data(), hash.size(), O_WRONLY | O_CREAT | O_TRUNC); QByteArray session_bytes(32, 0); QRandomGenerator::securelySeeded().fillRange(reinterpret_cast(session_bytes.data()), 8); std::string session_token = session_bytes.toHex().toStdString(); util::write_file(galaxy_session_path.c_str(), session_token.data(), session_token.size(), O_WRONLY | O_CREAT | O_TRUNC); static constexpr char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; std::string slug(16, '\0'); auto *rng = QRandomGenerator::global(); for (int i = 0; i < 16; ++i) { slug[i] = charset[rng->bounded(62)]; } util::write_file(galaxy_slug_path.c_str(), slug.data(), slug.size(), O_WRONLY | O_CREAT | O_TRUNC); pair_galaxy->setText(tr("UNPAIR")); galaxy_qr_btn->setVisible(true); showGalaxyQR(); return; } if (ConfirmationDialog::confirm(tr("Are you sure you want to unpair from Galaxy?"), tr("Unpair"), this)) { std::remove(galaxy_auth_path.c_str()); std::remove(galaxy_session_path.c_str()); std::remove(galaxy_slug_path.c_str()); pair_galaxy->setText(tr("PAIR")); galaxy_qr_btn->setVisible(false); } }); galaxy_qr_btn = new QPushButton(tr("QR")); galaxy_qr_btn->setFixedSize(250, 100); galaxy_qr_btn->setStyleSheet(R"( QPushButton { background-color: #393939; color: #E4E4E4; border-radius: 50px; font-size: 35px; font-weight: 500; } QPushButton:pressed { background-color: #4a4a4a; } )"); galaxy_qr_btn->setVisible(false); connect(galaxy_qr_btn, &QPushButton::clicked, showGalaxyQR); if (QHBoxLayout *hlayout = pair_galaxy->findChild()) { hlayout->insertWidget(3, galaxy_qr_btn); } const bool galaxy_paired = !util::read_file(galaxy_auth_path).empty(); pair_galaxy->setText(galaxy_paired ? tr("UNPAIR") : tr("PAIR")); galaxy_qr_btn->setVisible(galaxy_paired); addItem(pair_galaxy); // offroad-only buttons auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)")); connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); addItem(dcamBtn); resetDmCalibBtn = new ButtonControl( tr("Reset Driver Monitoring"), tr("RESET"), tr("Clears the saved driver monitoring wheel-side calibration and any manual right-hand-driving override if the device thinks you're seated on the wrong side. " "Resetting will restart openpilot if the car is powered on.") ); connect(resetDmCalibBtn, &ButtonControl::clicked, [&]() { if (!uiState()->engaged()) { if (ConfirmationDialog::confirm(tr("Are you sure you want to reset driver monitoring calibration?"), tr("Reset"), this)) { if (!uiState()->engaged()) { params.remove("IsRhdDetected"); params.remove("IsRHD"); params.remove("IsRHDOverride"); params.putBool("OnroadCycleRequested", true); } } } else { ConfirmationDialog::alert(tr("Disengage to Reset Driver Monitoring"), this); } }); addItem(resetDmCalibBtn); resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); connect(resetCalibBtn, &ButtonControl::clicked, [&]() { if (!uiState()->engaged()) { if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { // Check engaged again in case it changed while the dialog was open if (!uiState()->engaged()) { params.remove("CalibrationParams"); params.remove("LiveTorqueParameters"); params.remove("LiveParameters"); params.remove("LiveParametersV2"); params.remove("LiveDelay"); params.putBool("OnroadCycleRequested", true); updateCalibDescription(); } } } else { ConfirmationDialog::alert(tr("Disengage to Reset Calibration"), this); } }); addItem(resetCalibBtn); auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of openpilot")); connect(retrainingBtn, &ButtonControl::clicked, [=]() { if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { emit reviewTrainingGuide(); } }); addItem(retrainingBtn); if (Hardware::TICI()) { auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), ""); connect(regulatoryBtn, &ButtonControl::clicked, [=]() { const std::string txt = util::read_file("../assets/offroad/fcc.html"); ConfirmationDialog::rich(QString::fromStdString(txt), this); }); addItem(regulatoryBtn); } auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), ""); connect(translateBtn, &ButtonControl::clicked, [=]() { QMap langs = getSupportedLanguages(); QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), langs.key(uiState()->language), this); if (!selection.isEmpty()) { // put language setting, exit Qt UI, and trigger fast restart params.put("LanguageSetting", langs[selection].toStdString()); qApp->exit(18); watchdog_kick(0); } }); addItem(translateBtn); QObject::connect(uiState()->prime_state, &PrimeState::changed, [this] (PrimeState::Type type) { pair_device->setVisible(type == PrimeState::PRIME_TYPE_UNPAIRED); }); QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { if (btn != pair_device && btn != resetCalibBtn && btn != resetDmCalibBtn) { btn->setEnabled(offroad); } } }); // power buttons QHBoxLayout *power_layout = new QHBoxLayout(); power_layout->setSpacing(30); QPushButton *reboot_btn = new QPushButton(tr("Reboot")); reboot_btn->setObjectName("reboot_btn"); power_layout->addWidget(reboot_btn); QObject::connect(reboot_btn, &QPushButton::clicked, this, &DevicePanel::reboot); QPushButton *poweroff_btn = new QPushButton(tr("Power Off")); poweroff_btn->setObjectName("poweroff_btn"); power_layout->addWidget(poweroff_btn); QObject::connect(poweroff_btn, &QPushButton::clicked, this, &DevicePanel::poweroff); if (!Hardware::PC()) { connect(uiState(), &UIState::offroadTransition, poweroff_btn, &QPushButton::setVisible); } setStyleSheet(R"( #reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; } #reboot_btn:pressed { background-color: #4a4a4a; } #poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; } #poweroff_btn:pressed { background-color: #FF2424; } )"); addItem(power_layout); } void DevicePanel::updateCalibDescription() { QString desc = tr("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."); std::string calib_bytes = params.get("CalibrationParams"); if (!calib_bytes.empty()) { try { AlignedBuffer aligned_buf; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size())); auto calib = cmsg.getRoot().getLiveCalibration(); if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) { double pitch = calib.getRpyCalib()[1] * (180 / M_PI); double yaw = calib.getRpyCalib()[2] * (180 / M_PI); desc += tr(" Your device is pointed %1° %2 and %3° %4.") .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); } } catch (kj::Exception) { qInfo() << "invalid CalibrationParams"; } } int lag_perc = 0; std::string lag_bytes = params.get("LiveDelay"); if (!lag_bytes.empty()) { try { AlignedBuffer aligned_buf; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); } catch (kj::Exception) { qInfo() << "invalid LiveDelay"; } } if (lag_perc < 100) { desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc); } else { desc += tr("\n\nSteering lag calibration is complete."); } std::string torque_bytes = params.get("LiveTorqueParameters"); if (!torque_bytes.empty()) { try { AlignedBuffer aligned_buf; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size())); auto torque = cmsg.getRoot().getLiveTorqueParameters(); // don't add for non-torque cars if (torque.getUseParams()) { int torque_perc = torque.getCalPerc(); if (torque_perc < 100) { desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); } else { desc += tr(" Steering torque response calibration is complete."); } } } catch (kj::Exception) { qInfo() << "invalid LiveTorqueParameters"; } } desc += "\n\n"; desc += tr("openpilot is continuously calibrating, resetting is rarely required. " "Resetting calibration will restart openpilot if the car is powered on."); resetCalibBtn->setDescription(desc); } void DevicePanel::reboot() { if (!uiState()->engaged()) { if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), tr("Reboot"), this)) { // Check engaged again in case it changed while the dialog was open if (!uiState()->engaged()) { params.putBool("DoReboot", true); } } } else { ConfirmationDialog::alert(tr("Disengage to Reboot"), this); } } void DevicePanel::poweroff() { if (!uiState()->engaged()) { if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), tr("Power Off"), this)) { // Check engaged again in case it changed while the dialog was open if (!uiState()->engaged()) { params.putBool("DoShutdown", true); } } } else { ConfirmationDialog::alert(tr("Disengage to Power Off"), this); } } void SettingsWindow::showEvent(QShowEvent *event) { setCurrentPanel(0); } void SettingsWindow::hideEvent(QHideEvent *event) { closePanel(); closeSubPanel(); panelOpen = false; subPanelOpen = false; subSubPanelOpen = false; subSubSubPanelOpen = false; updateStarPilotToggles(); } void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { if (!param.isEmpty()) { // Check if param ends with "Panel" to determine if it's a panel name if (param.endsWith("Panel")) { QString panelName = param; panelName.chop(5); // Remove "Panel" suffix // Find the panel by name for (int i = 0; i < nav_btns->buttons().size(); i++) { if (nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) { index = i; break; } } } else { emit expandToggleDescription(param); emit scrollToToggle(param); } } panel_widget->setCurrentIndex(index); nav_btns->buttons()[index]->setChecked(true); } SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { // setup two main layouts sidebar_widget = new QWidget; QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); panel_widget = new QStackedWidget(); // close button QPushButton *close_btn = new QPushButton(tr("← Back")); close_btn->setStyleSheet(R"( QPushButton { font-size: 50px; border-radius: 25px; background-color: #292929; font-weight: 500; } QPushButton:pressed { background-color: #ADADAD; } )"); close_btn->setFixedSize(300, 125); sidebar_layout->addSpacing(10); sidebar_layout->addWidget(close_btn, 0, Qt::AlignRight); QObject::connect(close_btn, &QPushButton::clicked, [this]() { if (subSubSubPanelOpen) { closeSubSubSubPanel(); subSubSubPanelOpen = false; } else if (subSubPanelOpen) { closeSubSubPanel(); subSubPanelOpen = false; } else if (subPanelOpen) { closeSubPanel(); subPanelOpen = false; } else if (panelOpen) { closePanel(); panelOpen = false; } else { closeSettings(); } }); // setup panels DevicePanel *device = new DevicePanel(this); QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView); TogglesPanel *toggles = new TogglesPanel(this); QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle); auto networking = new Networking(this); QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); QObject::connect(toggles, &TogglesPanel::updateMetric, this, &SettingsWindow::updateMetric); StarPilotSettingsWindow *starpilotSettingsWindow = new StarPilotSettingsWindow(this); QObject::connect(starpilotSettingsWindow, &StarPilotSettingsWindow::openPanel, [this]() {panelOpen=true;}); QObject::connect(starpilotSettingsWindow, &StarPilotSettingsWindow::openSubPanel, [this]() {subPanelOpen=true;}); QObject::connect(starpilotSettingsWindow, &StarPilotSettingsWindow::openSubSubPanel, [this]() {subSubPanelOpen=true;}); QObject::connect(starpilotSettingsWindow, &StarPilotSettingsWindow::openSubSubSubPanel, [this]() {subSubSubPanelOpen=true;}); QObject::connect(starpilotSettingsWindow, &StarPilotSettingsWindow::tuningLevelChanged, this, &SettingsWindow::updateDeveloperToggle); DeveloperPanel *developerPanel = new DeveloperPanel(this); QObject::connect(developerPanel, &DeveloperPanel::openSubPanel, [this]() {subPanelOpen=true;}); QObject::connect(developerPanel, &DeveloperPanel::openSubSubPanel, [this]() {subSubPanelOpen=true;}); QObject::connect(developerPanel, &DeveloperPanel::showAllTogglesChanged, [this]() { updateDeveloperToggle(params.getInt("TuningLevel")); }); QObject::connect(toggles, &TogglesPanel::simpleModeChanged, [this](bool enabled) { updateDeveloperToggle(params.getInt("TuningLevel")); if (enabled && panel_widget->currentIndex() < nav_btns->buttons().size() && nav_btns->buttons()[panel_widget->currentIndex()]->text() == tr("StarPilot")) { setCurrentPanel(2); } }); QList> panels = { {tr("Device"), device}, {tr("Network"), networking}, {tr("Toggles"), toggles}, {tr("Software"), new SoftwarePanel(this)}, {tr("Developer"), developerPanel}, {tr("StarPilot"), starpilotSettingsWindow}, }; nav_btns = new QButtonGroup(this); for (auto &[name, panel] : panels) { QPushButton *btn = new QPushButton(name); btn->setCheckable(true); btn->setChecked(nav_btns->buttons().size() == 0); btn->setStyleSheet(R"( QPushButton { color: grey; border: none; background: none; font-size: 65px; font-weight: 500; } QPushButton:checked { color: white; } QPushButton:pressed { color: #ADADAD; } )"); btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); nav_btns->addButton(btn); sidebar_layout->addWidget(btn, 0, Qt::AlignRight); const int lr_margin = name != tr("Network") ? 50 : 0; // Network panel handles its own margins panel->setContentsMargins(lr_margin, 25, lr_margin, 25); ScrollView *panel_frame = new ScrollView(panel, this); panel_widget->addWidget(panel_frame); QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { if (w->widget() == starpilotSettingsWindow) { bool tuningLevelConfirmed = params.getBool("TuningLevelConfirmed"); if (!tuningLevelConfirmed) { int starpilotHours = QJsonDocument::fromJson(QString::fromStdString(params.get("StarPilotStats")).toUtf8()).object().value("StarPilotSeconds").toInt() / (60 * 60); int openpilotHours = params.getInt("KonikMinutes") / 60 + params.getInt("openpilotMinutes") / 60; if (starpilotHours < 1 && openpilotHours < 100) { if (openpilotHours < 10) { if (ConfirmationDialog::alert(tr("Welcome to StarPilot! Since you're new to openpilot, the \"Minimal\" toggle preset has been applied, but you can change this at any time via the \"Tuning Level\" button!"), this, true)) { params.putBool("TuningLevelConfirmed", true); params.putInt("TuningLevel", 0); } } else { if (ConfirmationDialog::alert(tr("Welcome to StarPilot! Since you're new to StarPilot, the \"Minimal\" toggle preset has been applied, but you can change this at any time via the \"Tuning Level\" button!"), this, true)) { params.putBool("TuningLevelConfirmed", true); params.putInt("TuningLevel", 0); } } } else if (starpilotHours < 50 && openpilotHours < 100) { if (ConfirmationDialog::alert(tr("Since you're fairly new to StarPilot, the \"Minimal\" toggle preset has been applied, but you can change this at any time via the \"Tuning Level\" button!"), this, true)) { params.putBool("TuningLevelConfirmed", true); params.putInt("TuningLevel", 0); } } else if (starpilotHours < 100) { if (openpilotHours >= 100) { if (ConfirmationDialog::alert(tr("Since you're experienced with openpilot, the \"Standard\" toggle preset has been applied, but you can change this at any time via the \"Tuning Level\" button!"), this, true)) { params.putBool("TuningLevelConfirmed", true); params.putInt("TuningLevel", 1); } } else { if (ConfirmationDialog::alert(tr("Since you're experienced with StarPilot, the \"Standard\" toggle preset has been applied, but you can change this at any time via the \"Tuning Level\" button!"), this, true)) { params.putBool("TuningLevelConfirmed", true); params.putInt("TuningLevel", 1); } } } else if (starpilotHours >= 100) { if (ConfirmationDialog::alert(tr("Since you're very experienced with StarPilot, the \"Advanced\" toggle preset has been applied, but you can change this at any time via the \"Tuning Level\" button!"), this, true)) { params.putBool("TuningLevelConfirmed", true); params.putInt("TuningLevel", 2); } } updateTuningLevel(); } } if (subSubSubPanelOpen) { closeSubSubSubPanel(); subSubSubPanelOpen = false; } if (subSubPanelOpen) { closeSubSubPanel(); subSubPanelOpen = false; } if (subPanelOpen) { closeSubPanel(); subPanelOpen = false; } if (panelOpen) { closePanel(); panelOpen = false; } btn->setChecked(true); panel_widget->setCurrentWidget(w); }); } sidebar_layout->setContentsMargins(50, 50, 100, 50); // main settings layout, sidebar + main panel QHBoxLayout *main_layout = new QHBoxLayout(this); sidebar_widget->setFixedWidth(500); main_layout->addWidget(sidebar_widget); main_layout->addWidget(panel_widget); setStyleSheet(R"( * { color: white; font-size: 50px; } SettingsWindow { background-color: black; } QStackedWidget, ScrollView { background-color: #292929; border-radius: 30px; } )"); updateDeveloperToggle(params.getInt("TuningLevel")); } void SettingsWindow::updateDeveloperToggle(int tuningLevel) { for (QAbstractButton *btn : nav_btns->buttons()) { if (btn->text() == tr("Developer")) { btn->setVisible(tuningLevel >= 3 || params.getBool("ShowAllToggles")); } else if (btn->text() == tr("StarPilot")) { btn->setVisible(!params.getBool("SimpleMode")); } } }