Files
StarPilot/selfdrive/ui/qt/offroad/settings.cc
T
2026-06-19 00:39:48 -05:00

892 lines
35 KiB
C++

#include <cassert>
#include <cmath>
#include <cstdio>
#include <string>
#include <tuple>
#include <vector>
#include <QCryptographicHash>
#include <QDebug>
#include <QRandomGenerator>
#include <QrCode.hpp>
#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<std::tuple<QString, QString, QString, QString, bool>> 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<QString> 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<int>(personality));
}
uiState()->scene.personality = personality;
}
}
void TogglesPanel::expandToggleDescription(const QString &param) {
toggles[param.toStdString()]->showDescription();
}
void TogglesPanel::scrollToToggle(const QString &param) {
if (auto it = toggles.find(param.toStdString()); it != toggles.end()) {
auto scroll_area = qobject_cast<QScrollArea*>(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<int>(cereal::LongitudinalPersonality::RELAXED)) {
params.putInt("LongitudinalPersonality", static_cast<int>(cereal::LongitudinalPersonality::RELAXED));
}
}
auto experimental_mode_toggle = toggles["ExperimentalMode"];
const QString e2e_description = QString("%1<br>"
"<h4>%2</h4><br>"
"%3<br>"
"<h4>%4</h4><br>"
"%5<br>")
.arg(tr("openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> 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<cereal::CarParams>();
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<int>(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("<b>" + long_desc + "</b><br><br>" + 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<int>(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();
QImage image(qr_size, qr_size, QImage::Format_RGB32);
for (int y = 0; y < qr_size; ++y) {
for (int x = 0; x < qr_size; ++x) {
image.setPixel(x, y, 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(400, 400, 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<quint32 *>(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<QHBoxLayout *>()) {
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<QString, QString> 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<ButtonControl *>()) {
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<cereal::Event>().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<cereal::Event>().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<cereal::Event>().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 &param) {
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<QPair<QString, QWidget *>> 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"));
}
}
}