diff --git a/frogpilot/assets/toggle_icons/icon_advanced_device.png b/frogpilot/assets/toggle_icons/icon_advanced_device.png new file mode 100644 index 000000000..f849ec132 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_advanced_device.png differ diff --git a/frogpilot/assets/toggle_icons/icon_advanced_lateral_tune.png b/frogpilot/assets/toggle_icons/icon_advanced_lateral_tune.png new file mode 100644 index 000000000..17ada9b7e Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_advanced_lateral_tune.png differ diff --git a/frogpilot/assets/toggle_icons/icon_advanced_longitudinal_tune.png b/frogpilot/assets/toggle_icons/icon_advanced_longitudinal_tune.png new file mode 100644 index 000000000..93aa435d7 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_advanced_longitudinal_tune.png differ diff --git a/frogpilot/assets/toggle_icons/icon_always_on_lateral.png b/frogpilot/assets/toggle_icons/icon_always_on_lateral.png new file mode 100644 index 000000000..dd67bba30 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_always_on_lateral.png differ diff --git a/frogpilot/assets/toggle_icons/icon_calendar.png b/frogpilot/assets/toggle_icons/icon_calendar.png new file mode 100644 index 000000000..85995051d Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_calendar.png differ diff --git a/frogpilot/assets/toggle_icons/icon_conditional.png b/frogpilot/assets/toggle_icons/icon_conditional.png new file mode 100644 index 000000000..18ec179d6 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_conditional.png differ diff --git a/frogpilot/assets/toggle_icons/icon_device.png b/frogpilot/assets/toggle_icons/icon_device.png new file mode 100644 index 000000000..2275347c4 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_device.png differ diff --git a/frogpilot/assets/toggle_icons/icon_display.png b/frogpilot/assets/toggle_icons/icon_display.png new file mode 100644 index 000000000..d6e6c1dfa Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_display.png differ diff --git a/frogpilot/assets/toggle_icons/icon_frog.png b/frogpilot/assets/toggle_icons/icon_frog.png new file mode 100644 index 000000000..3561616fb Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_frog.png differ diff --git a/frogpilot/assets/toggle_icons/icon_green_light.png b/frogpilot/assets/toggle_icons/icon_green_light.png new file mode 100644 index 000000000..b0c06f821 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_green_light.png differ diff --git a/frogpilot/assets/toggle_icons/icon_lane.png b/frogpilot/assets/toggle_icons/icon_lane.png new file mode 100644 index 000000000..066d1be59 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_lane.png differ diff --git a/frogpilot/assets/toggle_icons/icon_lateral_tune.png b/frogpilot/assets/toggle_icons/icon_lateral_tune.png new file mode 100644 index 000000000..8755ccddd Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_lateral_tune.png differ diff --git a/frogpilot/assets/toggle_icons/icon_light.png b/frogpilot/assets/toggle_icons/icon_light.png new file mode 100644 index 000000000..3467a1163 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_light.png differ diff --git a/frogpilot/assets/toggle_icons/icon_longitudinal_tune.png b/frogpilot/assets/toggle_icons/icon_longitudinal_tune.png new file mode 100644 index 000000000..2a16caac5 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_longitudinal_tune.png differ diff --git a/frogpilot/assets/toggle_icons/icon_map.png b/frogpilot/assets/toggle_icons/icon_map.png new file mode 100644 index 000000000..e952e5f63 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_map.png differ diff --git a/frogpilot/assets/toggle_icons/icon_message.png b/frogpilot/assets/toggle_icons/icon_message.png new file mode 100644 index 000000000..ec7097cf4 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_message.png differ diff --git a/frogpilot/assets/toggle_icons/icon_mute.png b/frogpilot/assets/toggle_icons/icon_mute.png new file mode 100644 index 000000000..7dccd2912 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_mute.png differ diff --git a/frogpilot/assets/toggle_icons/icon_navigate.png b/frogpilot/assets/toggle_icons/icon_navigate.png new file mode 100644 index 000000000..01e1a1b13 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_navigate.png differ diff --git a/frogpilot/assets/toggle_icons/icon_personality.png b/frogpilot/assets/toggle_icons/icon_personality.png new file mode 100644 index 000000000..d9fe2b777 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_personality.png differ diff --git a/frogpilot/assets/toggle_icons/icon_quality_of_life.png b/frogpilot/assets/toggle_icons/icon_quality_of_life.png new file mode 100644 index 000000000..4923b19a1 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_quality_of_life.png differ diff --git a/frogpilot/assets/toggle_icons/icon_rainbow.png b/frogpilot/assets/toggle_icons/icon_rainbow.png new file mode 100644 index 000000000..3b0379c23 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_rainbow.png differ diff --git a/frogpilot/assets/toggle_icons/icon_random.png b/frogpilot/assets/toggle_icons/icon_random.png new file mode 100644 index 000000000..b43568bb3 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_random.png differ diff --git a/frogpilot/assets/toggle_icons/icon_random_themes.png b/frogpilot/assets/toggle_icons/icon_random_themes.png new file mode 100644 index 000000000..e994abf0f Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_random_themes.png differ diff --git a/frogpilot/assets/toggle_icons/icon_road.png b/frogpilot/assets/toggle_icons/icon_road.png new file mode 100644 index 000000000..e1e44fd87 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_road.png differ diff --git a/frogpilot/assets/toggle_icons/icon_sound.png b/frogpilot/assets/toggle_icons/icon_sound.png new file mode 100644 index 000000000..68d0ace41 Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_sound.png differ diff --git a/frogpilot/assets/toggle_icons/icon_speed_limit.png b/frogpilot/assets/toggle_icons/icon_speed_limit.png new file mode 100644 index 000000000..3162f787a Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_speed_limit.png differ diff --git a/frogpilot/assets/toggle_icons/icon_speed_map.png b/frogpilot/assets/toggle_icons/icon_speed_map.png new file mode 100644 index 000000000..4557dd5fa Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_speed_map.png differ diff --git a/frogpilot/assets/toggle_icons/icon_steering.png b/frogpilot/assets/toggle_icons/icon_steering.png new file mode 100644 index 000000000..1f987418a Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_steering.png differ diff --git a/frogpilot/assets/toggle_icons/icon_system.png b/frogpilot/assets/toggle_icons/icon_system.png new file mode 100644 index 000000000..c3d82613f Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_system.png differ diff --git a/frogpilot/assets/toggle_icons/icon_tuning.png b/frogpilot/assets/toggle_icons/icon_tuning.png new file mode 100644 index 000000000..8c2fa95ca Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_tuning.png differ diff --git a/frogpilot/assets/toggle_icons/icon_vehicle.png b/frogpilot/assets/toggle_icons/icon_vehicle.png new file mode 100644 index 000000000..9acf8961f Binary files /dev/null and b/frogpilot/assets/toggle_icons/icon_vehicle.png differ diff --git a/frogpilot/ui/frogpilot_ui.cc b/frogpilot/ui/frogpilot_ui.cc index 4a075eb22..0dde77032 100644 --- a/frogpilot/ui/frogpilot_ui.cc +++ b/frogpilot/ui/frogpilot_ui.cc @@ -8,6 +8,7 @@ static void update_state(FrogPilotUIState *fs) { if (fpsm.updated("deviceState")) { const cereal::DeviceState::Reader &deviceState = fpsm["deviceState"].getDeviceState(); + frogpilot_scene.online = deviceState.getNetworkType() != cereal::DeviceState::NetworkType::NONE; } if (fpsm.updated("selfdriveState")) { const cereal::SelfdriveState::Reader &selfdriveState = fpsm["selfdriveState"].getSelfdriveState(); diff --git a/frogpilot/ui/frogpilot_ui.h b/frogpilot/ui/frogpilot_ui.h index a784f33d0..c35588e2d 100644 --- a/frogpilot/ui/frogpilot_ui.h +++ b/frogpilot/ui/frogpilot_ui.h @@ -7,6 +7,8 @@ struct FrogPilotUIScene { bool enabled; + bool frogpilot_panel_active; + bool online; bool parked; bool reverse; bool standstill; diff --git a/frogpilot/ui/qt/offroad/data_settings.cc b/frogpilot/ui/qt/offroad/data_settings.cc new file mode 100644 index 000000000..661f34166 --- /dev/null +++ b/frogpilot/ui/qt/offroad/data_settings.cc @@ -0,0 +1,12 @@ +#include "frogpilot/ui/qt/offroad/data_settings.h" + +FrogPilotDataPanel::FrogPilotDataPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *dataLayout = new QStackedLayout(); + addItem(dataLayout); + + FrogPilotListWidget *dataMainList = new FrogPilotListWidget(this); + ScrollView *dataMainPanel = new ScrollView(dataMainList, this); + dataLayout->addWidget(dataMainPanel); +} diff --git a/frogpilot/ui/qt/offroad/data_settings.h b/frogpilot/ui/qt/offroad/data_settings.h new file mode 100644 index 000000000..fe06cce48 --- /dev/null +++ b/frogpilot/ui/qt/offroad/data_settings.h @@ -0,0 +1,21 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotDataPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotDataPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + +private: + bool forceOpenDescriptions; + bool isMetric; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/device_settings.cc b/frogpilot/ui/qt/offroad/device_settings.cc new file mode 100644 index 000000000..0c957e6aa --- /dev/null +++ b/frogpilot/ui/qt/offroad/device_settings.cc @@ -0,0 +1,220 @@ +#include "frogpilot/ui/qt/offroad/device_settings.h" + +FrogPilotDevicePanel::FrogPilotDevicePanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *deviceLayout = new QStackedLayout(); + addItem(deviceLayout); + + FrogPilotListWidget *deviceList = new FrogPilotListWidget(this); + + ScrollView *devicePanel = new ScrollView(deviceList, this); + + deviceLayout->addWidget(devicePanel); + + FrogPilotListWidget *deviceManagementList = new FrogPilotListWidget(this); + FrogPilotListWidget *screenList = new FrogPilotListWidget(this); + + ScrollView *deviceManagementPanel = new ScrollView(deviceManagementList, this); + ScrollView *screenPanel = new ScrollView(screenList, this); + + deviceLayout->addWidget(deviceManagementPanel); + deviceLayout->addWidget(screenPanel); + + const std::vector> deviceToggles { + {"DeviceManagement", tr("Device Settings"), tr("Settings that control how the device runs, powers off, and manages driving data."), "../../frogpilot/assets/toggle_icons/icon_device.png"}, + {"DeviceShutdown", tr("Device Shutdown Timer"), tr("Keep the device on for the set amount of time after a drive before it shuts down automatically."), ""}, + {"NoLogging", tr("Disable Logging"), QString("%1

%2").arg(tr("WARNING: This will prevent your drives from being recorded and all data will be unobtainable!")).arg(tr("Prevent the device from saving driving data.")), ""}, + {"NoUploads", tr("Disable Uploads"), QString("%1

%2").arg(tr("WARNING: This will prevent your drives from being uploaded to comma connect which will impact debugging and official support from comma!")).arg(tr("Prevent the device from uploading driving data.")), ""}, + {"HigherBitrate", tr("High-Quality Recording"), tr("Save drive footage in higher video quality."), ""}, + {"LowVoltageShutdown", tr("Low-Voltage Cutoff"), tr("While parked, if the battery voltage falls below the set level, the device shuts down to prevent excessive battery drain."), ""}, + {"IncreaseThermalLimits", tr("Raise Temperature Limits"), QString("%1

%2").arg(tr("WARNING: Running at higher temperatures may damage your device!")).arg(tr("Allow the device to run at higher temperatures before throttling or shutting down. Use only if you understand the risks!")), ""}, + + {"ScreenManagement", tr("Screen Settings"), tr("Settings that control screen brightness, screen recording, and timeout duration."), "../../frogpilot/assets/toggle_icons/icon_light.png"}, + {"ScreenBrightness", tr("Screen Brightness (Offroad)"), tr("The screen brightness while not driving."), ""}, + {"ScreenBrightnessOnroad", tr("Screen Brightness (Onroad)"), tr("The screen brightness while driving."), ""}, + {"ScreenRecorder", tr("Screen Recorder"), tr("Add a button to the driving screen to record the display."), ""}, + {"ScreenTimeout", tr("Screen Timeout (Offroad)"), tr("How long the screen stays on after being tapped while not driving."), ""}, + {"ScreenTimeoutOnroad", tr("Screen Timeout (Onroad)"), tr("How long the screen stays on after being tapped while driving."), ""}, + {"StandbyMode", tr("Standby Mode"), tr("Turn the screen off while driving and automatically wake it up for alerts or engagement state changes."), ""}, + + {"IgnoreMe", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, + {"IgnoreMe2", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, + {"IgnoreMe3", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, + {"IgnoreMe4", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, + {"IgnoreMe5", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""} + }; + + for (const auto &[param, title, desc, icon] : deviceToggles) { + AbstractControl *deviceToggle; + + if (param == "DeviceManagement") { + FrogPilotManageControl *deviceManagementToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(deviceManagementToggle, &FrogPilotManageControl::manageButtonClicked, [deviceLayout, deviceManagementPanel]() { + deviceLayout->setCurrentWidget(deviceManagementPanel); + }); + deviceToggle = 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")); + } + deviceToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 33, QString(), shutdownLabels); + } else if (param == "NoUploads") { + std::vector uploadsToggles{"DisableOnroadUploads"}; + std::vector uploadsToggleNames{tr("Disable Onroad Only")}; + deviceToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, uploadsToggles, uploadsToggleNames); + } else if (param == "LowVoltageShutdown") { + deviceToggle = new FrogPilotParamValueControl(param, title, desc, icon, 11.8, 12.5, tr(" volts"), std::map(), 0.1); + + } else if (param == "ScreenManagement") { + FrogPilotManageControl *screenToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(screenToggle, &FrogPilotManageControl::manageButtonClicked, [deviceLayout, screenPanel]() { + deviceLayout->setCurrentWidget(screenPanel); + }); + deviceToggle = screenToggle; + } else if (param == "ScreenBrightness" || param == "ScreenBrightnessOnroad") { + std::map brightnessLabels; + int minBrightness = (param == "ScreenBrightnessOnroad") ? 0 : 1; + for (int i = 0; i <= 101; ++i) { + brightnessLabels[i] = i == 0 ? tr("Screen Off") : i == 101 ? tr("Auto") : QString::number(i) + "%"; + } + deviceToggle = new FrogPilotParamValueControl(param, title, desc, icon, minBrightness, 101, QString(), brightnessLabels, 1, true); + } else if (param == "ScreenTimeout" || param == "ScreenTimeoutOnroad") { + deviceToggle = new FrogPilotParamValueControl(param, title, desc, icon, 5, 60, tr(" seconds"), {}, 5); + + } else { + deviceToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = deviceToggle; + + if (deviceManagementKeys.contains(param)) { + deviceManagementList->addItem(deviceToggle); + } else if (screenKeys.contains(param)) { + screenList->addItem(deviceToggle); + } else { + deviceList->addItem(deviceToggle); + + parentKeys.insert(param); + } + + if (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(deviceToggle)) { + QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + emit openSubPanel(); + openDescriptions(forceOpenDescriptions, toggles); + }); + } + + QObject::connect(deviceToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(deviceToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + static_cast(toggles["IncreaseThermalLimits"])->setConfirmation(true, false); + static_cast(toggles["NoLogging"])->setConfirmation(true, false); + static_cast(toggles["NoUploads"])->setConfirmation(true, false); + + QSet brightnessKeys = {"ScreenBrightness", "ScreenBrightnessOnroad"}; + for (const QString &key : brightnessKeys) { + FrogPilotParamValueControl *paramControl = static_cast(toggles[key]); + QObject::connect(paramControl, &FrogPilotParamValueControl::valueChanged, [key, this](int value) { + if (!started && key == "ScreenBrightness") { + Hardware::set_brightness(value); + } else if (started && key == "ScreenBrightnessOnroad") { + Hardware::set_brightness(value); + } + }); + } + + QSet forceUpdateKeys = {"NoUploads"}; + for (const QString &key : forceUpdateKeys) { + QObject::connect(static_cast(toggles[key]), &FrogPilotButtonToggleControl::buttonClicked, this, &FrogPilotDevicePanel::updateToggles); + QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, this, &FrogPilotDevicePanel::updateToggles); + } + + QSet rebootKeys = {"HigherBitrate"}; + for (const QString &key : rebootKeys) { + QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, [key, this](bool state) { + QString filePath; + if (key == "HigherBitrate") { + filePath = "/cache/use_HD"; + } + + if (!filePath.isEmpty()) { + QFile toggleFile(filePath); + if (state) { + if (!toggleFile.exists()) { + toggleFile.open(QIODevice::WriteOnly); + toggleFile.close(); + } + } else { + if (toggleFile.exists()) { + toggleFile.remove(); + } + } + } + + if (FrogPilotConfirmationDialog::toggleReboot(this)) { + Hardware::reboot(); + } + }); + } + + openDescriptions(forceOpenDescriptions, toggles); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [deviceLayout, devicePanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + deviceLayout->setCurrentWidget(devicePanel); + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotDevicePanel::updateState); +} + +void FrogPilotDevicePanel::showEvent(QShowEvent *event) { + updateToggles(); +} + +void FrogPilotDevicePanel::updateState(const UIState &s) { + if (!isVisible()) { + return; + } + + started = s.scene.started; +} + +void FrogPilotDevicePanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "HigherBitrate") { + setVisible &= params.getBool("DeviceManagement") && params.getBool("NoUploads") && !params.getBool("DisableOnroadUploads"); + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (deviceManagementKeys.contains(key)) { + toggles["DeviceManagement"]->setVisible(true); + } else if (screenKeys.contains(key)) { + toggles["ScreenManagement"]->setVisible(true); + } + } + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/device_settings.h b/frogpilot/ui/qt/offroad/device_settings.h new file mode 100644 index 000000000..7a281bae7 --- /dev/null +++ b/frogpilot/ui/qt/offroad/device_settings.h @@ -0,0 +1,34 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotDevicePanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotDevicePanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateState(const UIState &s); + void updateToggles(); + + bool forceOpenDescriptions; + bool started; + + std::map toggles; + + QSet deviceManagementKeys = {"DeviceShutdown", "HigherBitrate", "IncreaseThermalLimits", "LowVoltageShutdown", "NoLogging", "NoUploads"}; + QSet screenKeys = {"ScreenBrightness", "ScreenBrightnessOnroad", "ScreenRecorder", "ScreenTimeout", "ScreenTimeoutOnroad", "StandbyMode"}; + + QSet parentKeys; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/frogpilot_settings.cc b/frogpilot/ui/qt/offroad/frogpilot_settings.cc new file mode 100644 index 000000000..64f365351 --- /dev/null +++ b/frogpilot/ui/qt/offroad/frogpilot_settings.cc @@ -0,0 +1,425 @@ +#include "frogpilot/ui/qt/offroad/data_settings.h" +#include "frogpilot/ui/qt/offroad/device_settings.h" +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" +#include "frogpilot/ui/qt/offroad/lateral_settings.h" +#include "frogpilot/ui/qt/offroad/longitudinal_settings.h" +#include "frogpilot/ui/qt/offroad/maps_settings.h" +#include "frogpilot/ui/qt/offroad/model_settings.h" +#include "frogpilot/ui/qt/offroad/navigation_settings.h" +#include "frogpilot/ui/qt/offroad/sounds_settings.h" +#include "frogpilot/ui/qt/offroad/theme_settings.h" +#include "frogpilot/ui/qt/offroad/utilities.h" +#include "frogpilot/ui/qt/offroad/vehicle_settings.h" +#include "frogpilot/ui/qt/offroad/visual_settings.h" +#include "frogpilot/ui/qt/offroad/wheel_settings.h" + +void FrogPilotSettingsWindow::createPanelButtons(FrogPilotListWidget *list) { + FrogPilotDataPanel *frogpilotDataPanel = new FrogPilotDataPanel(this, !shownDescriptions.value("FrogPilotDataPanel").toBool(false)); + FrogPilotDevicePanel *frogpilotDevicePanel = new FrogPilotDevicePanel(this, !shownDescriptions.value("FrogPilotDevicePanel").toBool(false)); + FrogPilotLateralPanel *frogpilotLateralPanel = new FrogPilotLateralPanel(this, !shownDescriptions.value("FrogPilotLateralPanel").toBool(false)); + FrogPilotLongitudinalPanel *frogpilotLongitudinalPanel = new FrogPilotLongitudinalPanel(this, !shownDescriptions.value("FrogPilotLongitudinalPanel").toBool(false)); + FrogPilotMapsPanel *frogpilotMapsPanel = new FrogPilotMapsPanel(this, !shownDescriptions.value("FrogPilotMapsPanel").toBool(false)); + FrogPilotModelPanel *frogpilotModelPanel = new FrogPilotModelPanel(this, !shownDescriptions.value("FrogPilotModelPanel").toBool(false)); + FrogPilotNavigationPanel *frogpilotNavigationPanel = new FrogPilotNavigationPanel(this, !shownDescriptions.value("FrogPilotNavigationPanel").toBool(false)); + FrogPilotSoundsPanel *frogpilotSoundsPanel = new FrogPilotSoundsPanel(this, !shownDescriptions.value("FrogPilotSoundsPanel").toBool(false)); + FrogPilotThemesPanel *frogpilotThemesPanel = new FrogPilotThemesPanel(this, !shownDescriptions.value("FrogPilotThemesPanel").toBool(false)); + FrogPilotUtilitiesPanel *frogpilotUtilitiesPanel = new FrogPilotUtilitiesPanel(this, !shownDescriptions.value("FrogPilotUtilitiesPanel").toBool(false)); + FrogPilotVehiclesPanel *frogpilotVehiclesPanel = new FrogPilotVehiclesPanel(this, !shownDescriptions.value("FrogPilotVehiclesPanel").toBool(false)); + FrogPilotVisualsPanel *frogpilotVisualsPanel = new FrogPilotVisualsPanel(this, !shownDescriptions.value("FrogPilotVisualsPanel").toBool(false)); + FrogPilotWheelPanel *frogpilotWheelPanel = new FrogPilotWheelPanel(this, !shownDescriptions.value("FrogPilotWheelPanel").toBool(false)); + + std::vector>> panelButtons = { + {{tr("MANAGE"), frogpilotSoundsPanel}}, + {{tr("DRIVING MODEL"), frogpilotModelPanel}, {tr("GAS / BRAKE"), frogpilotLongitudinalPanel}, {tr("STEERING"), frogpilotLateralPanel}}, + {{tr("MAP DATA"), frogpilotMapsPanel}, {tr("NAVIGATION"), frogpilotNavigationPanel}}, + {{tr("DATA"), frogpilotDataPanel}, {tr("DEVICE CONTROLS"), frogpilotDevicePanel}, {tr("UTILITIES"), frogpilotUtilitiesPanel}}, + {{tr("APPEARANCE"), frogpilotVisualsPanel}, {tr("THEME"), frogpilotThemesPanel}}, + {{tr("VEHICLE SETTINGS"), frogpilotVehiclesPanel}, {tr("WHEEL CONTROLS"), frogpilotWheelPanel}} + }; + + std::vector> panelInfo = { + {tr("Alerts and Sounds"), tr("Adjust alert volumes and enable custom notifications."), "../../frogpilot/assets/toggle_icons/icon_sound.png"}, + {tr("Driving Controls"), tr("Fine-tune custom FrogPilot acceleration, braking, and steering controls."), "../../frogpilot/assets/toggle_icons/icon_steering.png"}, + {tr("Navigation"), tr("Download map data for the \"Speed Limit Controller\"."), "../../frogpilot/assets/toggle_icons/icon_navigate.png"}, + {tr("System Settings"), tr("Manage backups, device settings, screen options, storage, and tools to keep FrogPilot running smoothly."), "../../frogpilot/assets/toggle_icons/icon_system.png"}, + {tr("Theme and Appearance"), tr("Customize the look of the driving screen and interface, including themes!"), "../../frogpilot/assets/toggle_icons/icon_display.png"}, + {tr("Vehicle Settings"), tr("Configure car-specific options and steering wheel button mappings."), "../../frogpilot/assets/toggle_icons/icon_vehicle.png"} + }; + + for (size_t i = 0; i < panelInfo.size(); ++i) { + const QString &title = std::get<0>(panelInfo[i]); + const QString &description = std::get<1>(panelInfo[i]); + const QString &icon = std::get<2>(panelInfo[i]); + + const std::vector> &widgetLabels = panelButtons[i]; + + std::vector labels; + std::vector widgets; + + for (size_t j = 0; j < widgetLabels.size(); ++j) { + labels.push_back(std::get<0>(widgetLabels[j])); + + QWidget *panel = std::get<1>(widgetLabels[j]); + panel->setContentsMargins(50, 25, 50, 25); + + ScrollView *panelFrame = new ScrollView(panel, this); + mainLayout->addWidget(panelFrame); + widgets.push_back(panelFrame); + } + + FrogPilotButtonsControl *panelButton = new FrogPilotButtonsControl(title, description, icon, labels); + if (title == tr("Alerts and Sounds")) soundPanelButtons = panelButton; + if (title == tr("Driving Controls")) drivingPanelButtons = panelButton; + if (title == tr("Navigation")) navigationPanelButtons = panelButton; + if (title == tr("System Settings")) systemPanelButtons = panelButton; + if (title == tr("Theme and Appearance")) themePanelButtons = panelButton; + if (title == tr("Vehicle Settings")) vehiclePanelButtons = panelButton; + + if (forceOpenDescriptions) { + panelButton->showDescription(); + } + + QObject::connect(panelButton, &FrogPilotButtonsControl::buttonClicked, [widgets, this](int id) { + mainLayout->setCurrentWidget(widgets[id]); + + panelOpen = true; + + openPanel(); + + ScrollView *panelFrame = qobject_cast(widgets[id]); + if (panelFrame) { + QWidget *panel = panelFrame->widget(); + QString className = panel->metaObject()->className(); + + if (!shownDescriptions.value(className).toBool(false)) { + shownDescriptions.insert(className, true); + params.putNonBlocking("ShownToggleDescriptions", QJsonDocument(shownDescriptions).toJson(QJsonDocument::Compact).toStdString()); + } + } + }); + + list->addItem(panelButton); + } + + QObject::connect(frogpilotDataPanel, &FrogPilotDataPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotDevicePanel, &FrogPilotDevicePanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotLateralPanel, &FrogPilotLateralPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotLongitudinalPanel, &FrogPilotLongitudinalPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotLongitudinalPanel, &FrogPilotLongitudinalPanel::openSubSubPanel, this, &FrogPilotSettingsWindow::openSubSubPanel); + QObject::connect(frogpilotLongitudinalPanel, &FrogPilotLongitudinalPanel::openSubSubSubPanel, this, &FrogPilotSettingsWindow::openSubSubSubPanel); + QObject::connect(frogpilotMapsPanel, &FrogPilotMapsPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotModelPanel, &FrogPilotModelPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotNavigationPanel, &FrogPilotNavigationPanel::closeSubPanel, this, &FrogPilotSettingsWindow::closeSubPanel); + QObject::connect(frogpilotNavigationPanel, &FrogPilotNavigationPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotSoundsPanel, &FrogPilotSoundsPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotThemesPanel, &FrogPilotThemesPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotVehiclesPanel, &FrogPilotVehiclesPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotVisualsPanel, &FrogPilotVisualsPanel::openSubPanel, this, &FrogPilotSettingsWindow::openSubPanel); + QObject::connect(frogpilotVisualsPanel, &FrogPilotVisualsPanel::openSubSubPanel, this, &FrogPilotSettingsWindow::openSubSubPanel); +} + +FrogPilotSettingsWindow::FrogPilotSettingsWindow(SettingsWindow *parent) : QFrame(parent) { + shownDescriptions = QJsonDocument::fromJson(QString::fromStdString(params.get("ShownToggleDescriptions")).toUtf8()).object(); + + QString className = this->metaObject()->className(); + if (!shownDescriptions.value(className).toBool(false)) { + forceOpenDescriptions = true; + } + + mainLayout = new QStackedLayout(this); + + QWidget *frogpilotWidget = new QWidget(this); + QVBoxLayout *frogpilotLayout = new QVBoxLayout(frogpilotWidget); + frogpilotLayout->setContentsMargins(50, 25, 50, 25); + frogpilotWidget->setLayout(frogpilotLayout); + + frogpilotPanel = new ScrollView(frogpilotWidget, this); + mainLayout->addWidget(frogpilotPanel); + frogpilotPanel->setWidget(frogpilotWidget); + + FrogPilotListWidget *list = new FrogPilotListWidget(this); + frogpilotLayout->addWidget(list); + + std::vector togglePresets{tr("Minimal"), tr("Standard"), tr("Advanced"), tr("Developer")}; + togglePreset = new FrogPilotButtonsControl(tr("Tuning Level"), + tr("Choose your tuning level. Lower levels keep it simple; higher levels unlock more toggles for finer control.\n\n" + "Minimal - Ideal for those who prefer simplicity or ease of use\n" + "Standard - Recommended for most users for a balanced experience\n" + "Advanced - Fine-tuning for experienced users\n" + "Developer - Highly customizable settings for seasoned enthusiasts"), + "../../frogpilot/assets/toggle_icons/icon_tuning.png", togglePresets, true); + QObject::connect(togglePreset, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + tuningLevel = id; + + params.putIntNonBlocking("TuningLevel", tuningLevel); + + updateVariables(); + + emit tuningLevelChanged(tuningLevel); + + if (id == 3) { + ConfirmationDialog::alert(tr("WARNING: These settings are risky and can drastically change how openpilot drives. Only change if you fully understand what they do!"), this); + } + }); + togglePreset->setCheckedButton(params.getInt("TuningLevel")); + if (forceOpenDescriptions) { + togglePreset->showDescription(); + } + list->addItem(togglePreset, true); + + createPanelButtons(list); + + QObject::connect(parent, &SettingsWindow::closePanel, this, &FrogPilotSettingsWindow::closePanel); + QObject::connect(parent, &SettingsWindow::closeSubPanel, this, &FrogPilotSettingsWindow::closeSubPanel); + QObject::connect(parent, &SettingsWindow::closeSubSubPanel, this, &FrogPilotSettingsWindow::closeSubSubPanel); + QObject::connect(parent, &SettingsWindow::closeSubSubSubPanel, this, &FrogPilotSettingsWindow::closeSubSubSubPanel); + QObject::connect(parent, &SettingsWindow::updateMetric, this, &FrogPilotSettingsWindow::updateMetric); + QObject::connect(parent, &SettingsWindow::updateTuningLevel, this, &FrogPilotSettingsWindow::updateTuningLevel); + QObject::connect(uiState(), &UIState::offroadTransition, this, &FrogPilotSettingsWindow::updateVariables); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotSettingsWindow::updateState); + + std::vector keys = params.allKeys(); + for (std::vector::const_iterator it = keys.begin(); it != keys.end(); ++it) { + const std::string &key = *it; + frogpilotToggleLevels[QString::fromStdString(key)] = params.getTuningLevel(key); + } + tuningLevel = params.getInt("TuningLevel"); + + closeSubPanel(); + updateMetric(params.getBool("IsMetric"), true); + updateVariables(); +} + +void FrogPilotSettingsWindow::updateTuningLevel() { + tuningLevel = params.getInt("TuningLevel"); + togglePreset->setCheckedButton(params.getInt("TuningLevel")); + + updateVariables(); + + emit tuningLevelChanged(tuningLevel); +} + +void FrogPilotSettingsWindow::showEvent(QShowEvent *event) { + static bool alertShown = false; + + QString className = this->metaObject()->className(); + if (!shownDescriptions.value(className).toBool(false)) { + shownDescriptions.insert(className, true); + params.putNonBlocking("ShownToggleDescriptions", QJsonDocument(shownDescriptions).toJson(QJsonDocument::Compact).toStdString()); + } + + if (forceOpenDescriptions) { + togglePreset->showDescription(); + + drivingPanelButtons->showDescription(); + navigationPanelButtons->showDescription(); + soundPanelButtons->showDescription(); + systemPanelButtons->showDescription(); + themePanelButtons->showDescription(); + vehiclePanelButtons->showDescription(); + + if (!alertShown) { + ConfirmationDialog::alert(tr("All toggle descriptions are currently expanded. You can tap a toggle's name to open or close its description at any time!"), this); + alertShown = true; + } + } +} + +void FrogPilotSettingsWindow::hideEvent(QHideEvent *event) { + closePanel(); +} + +void FrogPilotSettingsWindow::closePanel() { + if (forceOpenDescriptions) { + togglePreset->showDescription(); + + drivingPanelButtons->showDescription(); + navigationPanelButtons->showDescription(); + soundPanelButtons->showDescription(); + systemPanelButtons->showDescription(); + themePanelButtons->showDescription(); + vehiclePanelButtons->showDescription(); + } + + mainLayout->setCurrentWidget(frogpilotPanel); + + panelOpen = false; +} + +void FrogPilotSettingsWindow::updateState() { + FrogPilotUIState &fs = *frogpilotUIState(); + FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + frogpilot_scene.frogpilot_panel_active = panelOpen && keepScreenOn; +} + +void FrogPilotSettingsWindow::updateVariables() { + FrogPilotUIState &fs = *frogpilotUIState(); + FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + QJsonObject &frogpilot_toggles = frogpilot_scene.frogpilot_toggles; + + isFrogsGoMoo = ::isFrogsGoMoo(); + + std::string 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(); + cereal::CarParams::SafetyModel safetyModel = CP.getSafetyConfigs()[0].getSafetyModel(); + + std::string carFingerprint = CP.getCarFingerprint(); + carMake = CP.getBrand(); + + friction = CP.getLateralTuning().getTorque().getFriction(); + hasAlphaLongitudinal = CP.getAlphaLongitudinalAvailable(); + hasBSM = CP.getEnableBsm(); + hasDashSpeedLimits = carMake == "ford" || carMake == "hyundai" || carMake == "toyota"; + hasOpenpilotLongitudinal = hasLongitudinalControl(CP); + hasPCMCruise = CP.getPcmCruise(); + hasPedal = CP.getEnableGasInterceptorDEPRECATED(); + hasRadar = !CP.getRadarUnavailable(); + hasSDSU = frogpilot_toggles.value("has_sdsu").toBool(); + hasSNG = CP.getAutoResumeSng(); + hasZSS = frogpilot_toggles.value("has_zss").toBool(); + isAngleCar = CP.getSteerControlType() == cereal::CarParams::SteerControlType::ANGLE; + isBolt = carFingerprint == "CHEVROLET_BOLT_CC" || carFingerprint == "CHEVROLET_BOLT_EUV"; + isGM = carMake == "gm"; + isHKG = carMake == "hyundai"; + isHKGCanFd = isHKG && safetyModel == cereal::CarParams::SafetyModel::HYUNDAI_CANFD; + isSubaru = carMake == "subaru"; + isTorqueCar = CP.getLateralTuning().which() == cereal::CarParams::LateralTuning::TORQUE; + isToyota = carMake == "toyota"; + isTSK = CP.getSecOcRequired(); + isVolt = carFingerprint == "CHEVROLET_VOLT"; + latAccelFactor = CP.getLateralTuning().getTorque().getLatAccelFactor(); + lkasAllowedForAOL = frogpilot_toggles.value("lkas_allowed_for_aol").toBool(); + longitudinalActuatorDelay = CP.getLongitudinalActuatorDelay(); + startAccel = CP.getStartAccel(); + steerActuatorDelay = CP.getSteerActuatorDelay(); + steerKp = 1.0f; + steerRatio = CP.getSteerRatio(); + stopAccel = CP.getStopAccel(); + stoppingDecelRate = CP.getStoppingDecelRate(); + vEgoStarting = CP.getVEgoStarting(); + vEgoStopping = CP.getVEgoStopping(); + + float currentDelayStock = params.getFloat("SteerDelayStock"); + float currentFrictionStock = params.getFloat("SteerFrictionStock"); + float currentKPStock = params.getFloat("SteerKPStock"); + float currentLatAccelStock = params.getFloat("SteerLatAccelStock"); + float currentLongDelayStock = params.getFloat("LongitudinalActuatorDelayStock"); + float currentStartAccelStock = params.getFloat("StartAccelStock"); + float currentSteerRatioStock = params.getFloat("SteerRatioStock"); + float currentStopAccelStock = params.getFloat("StopAccelStock"); + float currentStoppingDecelRateStock = params.getFloat("StoppingDecelRateStock"); + float currentVEgoStartingStock = params.getFloat("VEgoStartingStock"); + float currentVEgoStoppingStock = params.getFloat("VEgoStoppingStock"); + + if (currentDelayStock != steerActuatorDelay && steerActuatorDelay != 0) { + if (params.getFloat("SteerDelay") == currentDelayStock || currentDelayStock == 0) { + params.putFloatNonBlocking("SteerDelay", steerActuatorDelay); + } + params.putFloatNonBlocking("SteerDelayStock", steerActuatorDelay); + } + + if (currentFrictionStock != friction && friction != 0) { + if (params.getFloat("SteerFriction") == currentFrictionStock || currentFrictionStock == 0) { + params.putFloatNonBlocking("SteerFriction", friction); + } + params.putFloatNonBlocking("SteerFrictionStock", friction); + } + + if (currentKPStock != steerKp && steerKp != 0) { + if (params.getFloat("SteerKP") == currentKPStock || currentKPStock == 0) { + params.putFloatNonBlocking("SteerKP", steerKp); + } + params.putFloatNonBlocking("SteerKPStock", steerKp); + } + + if (currentLatAccelStock != latAccelFactor && latAccelFactor != 0) { + if (params.getFloat("SteerLatAccel") == currentLatAccelStock || currentLatAccelStock == 0) { + params.putFloatNonBlocking("SteerLatAccel", latAccelFactor); + } + params.putFloatNonBlocking("SteerLatAccelStock", latAccelFactor); + } + + if (currentLongDelayStock != longitudinalActuatorDelay && longitudinalActuatorDelay != 0) { + if (params.getFloat("LongitudinalActuatorDelay") == currentLongDelayStock || currentLongDelayStock == 0) { + params.putFloatNonBlocking("LongitudinalActuatorDelay", longitudinalActuatorDelay); + } + params.putFloatNonBlocking("LongitudinalActuatorDelayStock", longitudinalActuatorDelay); + } + + if (currentStartAccelStock != startAccel && startAccel != 0) { + if (params.getFloat("StartAccel") == currentStartAccelStock || currentStartAccelStock == 0) { + params.putFloatNonBlocking("StartAccel", startAccel); + } + params.putFloatNonBlocking("StartAccelStock", startAccel); + } + + if (currentSteerRatioStock != steerRatio && steerRatio != 0) { + if (params.getFloat("SteerRatio") == currentSteerRatioStock || currentSteerRatioStock == 0) { + params.putFloatNonBlocking("SteerRatio", steerRatio); + } + params.putFloatNonBlocking("SteerRatioStock", steerRatio); + } + + if (currentStopAccelStock != stopAccel && stopAccel != 0) { + if (params.getFloat("StopAccel") == currentStopAccelStock || currentStopAccelStock == 0) { + params.putFloatNonBlocking("StopAccel", stopAccel); + } + params.putFloatNonBlocking("StopAccelStock", stopAccel); + } + + if (currentStoppingDecelRateStock != stoppingDecelRate && stoppingDecelRate != 0) { + if (params.getFloat("StoppingDecelRate") == currentStoppingDecelRateStock || currentStoppingDecelRateStock == 0) { + params.putFloatNonBlocking("StoppingDecelRate", stoppingDecelRate); + } + params.putFloatNonBlocking("StoppingDecelRateStock", stoppingDecelRate); + } + + if (currentVEgoStartingStock != vEgoStarting && vEgoStarting != 0) { + if (params.getFloat("VEgoStarting") == currentVEgoStartingStock || currentVEgoStartingStock == 0) { + params.putFloatNonBlocking("VEgoStarting", vEgoStarting); + } + params.putFloatNonBlocking("VEgoStartingStock", vEgoStarting); + } + + if (currentVEgoStoppingStock != vEgoStopping && vEgoStopping != 0) { + if (params.getFloat("VEgoStopping") == currentVEgoStoppingStock || currentVEgoStoppingStock == 0) { + params.putFloatNonBlocking("VEgoStopping", vEgoStopping); + } + params.putFloatNonBlocking("VEgoStoppingStock", vEgoStopping); + } + } + + std::string frogpilotCarParams = params.get("FrogPilotCarParamsPersistent"); + if (!frogpilotCarParams.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader fpcmsg(aligned_buf.align(frogpilotCarParams.data(), frogpilotCarParams.size())); + cereal::FrogPilotCarParams::Reader FPCP = fpcmsg.getRoot(); + + canUsePedal = FPCP.getCanUsePedal(); + canUseSDSU = FPCP.getCanUseSDSU(); + openpilotLongitudinalControlDisabled = FPCP.getOpenpilotLongitudinalControlDisabled(); + } + + std::string liveTorqueParameters = params.get("LiveTorqueParameters"); + if (!liveTorqueParameters.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader reader(aligned_buf.align(liveTorqueParameters.data(), liveTorqueParameters.size())); + cereal::Event::Reader event = reader.getRoot(); + cereal::LiveTorqueParametersData::Reader LTP = event.getLiveTorqueParameters(); + + hasAutoTune = LTP.getUseParams(); + } + + drivingPanelButtons->setVisibleButton(0, tuningLevel >= frogpilotToggleLevels.value("DrivingModel").toDouble()); + drivingPanelButtons->setVisibleButton(1, hasOpenpilotLongitudinal); + + systemPanelButtons->setVisibleButton(1, tuningLevel >= frogpilotToggleLevels.value("DeviceManagement").toDouble() || tuningLevel >= frogpilotToggleLevels.value("ScreenManagement").toDouble()); + + vehiclePanelButtons->setVisibleButton(1, tuningLevel >= frogpilotToggleLevels.value("WheelControls").toDouble()); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/frogpilot_settings.h b/frogpilot/ui/qt/offroad/frogpilot_settings.h new file mode 100644 index 000000000..a8b5927f3 --- /dev/null +++ b/frogpilot/ui/qt/offroad/frogpilot_settings.h @@ -0,0 +1,103 @@ +#pragma once + +#include "selfdrive/ui/qt/offroad/settings.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" + +#include "frogpilot/ui/frogpilot_ui.h" +#include "frogpilot/ui/qt/widgets/frogpilot_controls.h" + +class QNetworkAccessManager; + +class FrogPilotSettingsWindow : public QFrame { + Q_OBJECT + +public: + explicit FrogPilotSettingsWindow(SettingsWindow *parent); + + void updateVariables(); + + bool canUsePedal = false; + bool canUseSDSU = false; + bool forceOpenDescriptions = false; + bool hasAlphaLongitudinal = false; + bool hasAutoTune = true; + bool hasBSM = true; + bool hasDashSpeedLimits = true; + bool hasNNFFLog = true; + bool hasOpenpilotLongitudinal = true; + bool hasPCMCruise = false; + bool hasPedal = false; + bool hasRadar = true; + bool hasSDSU = false; + bool hasSNG = false; + bool hasZSS = false; + bool isAngleCar = false; + bool isBolt = false; + bool isFrogsGoMoo = false; + bool isGM = true; + bool isHKG = true; + bool isHKGCanFd = true; + bool isSubaru = false; + bool isTorqueCar = false; + bool isToyota = true; + bool isTSK = false; + bool isVolt = true; + bool keepScreenOn = false; + bool lkasAllowedForAOL = false; + bool openpilotLongitudinalControlDisabled = false; + + float friction; + float latAccelFactor; + float longitudinalActuatorDelay; + float startAccel; + float steerActuatorDelay; + float steerKp; + float steerRatio; + float stopAccel; + float stoppingDecelRate; + float vEgoStarting; + float vEgoStopping; + + int tuningLevel; + + QJsonObject frogpilotToggleLevels; + +signals: + void closeSubPanel(); + void closeSubSubPanel(); + void closeSubSubSubPanel(); + void openPanel(); + void openSubPanel(); + void openSubSubPanel(); + void openSubSubSubPanel(); + void tuningLevelChanged(int level); + void updateMetric(bool metric, bool bootRun=false); + +private: + void closePanel(); + void createPanelButtons(FrogPilotListWidget *list); + void hideEvent(QHideEvent *event) override; + void showEvent(QShowEvent *event) override; + void updateState(); + void updateTuningLevel(); + + bool panelOpen; + + std::string carMake; + + FrogPilotButtonsControl *drivingPanelButtons; + FrogPilotButtonsControl *navigationPanelButtons; + FrogPilotButtonsControl *soundPanelButtons; + FrogPilotButtonsControl *systemPanelButtons; + FrogPilotButtonsControl *themePanelButtons; + FrogPilotButtonsControl *togglePreset; + FrogPilotButtonsControl *vehiclePanelButtons; + + Params params; + + QJsonObject shownDescriptions; + + QStackedLayout *mainLayout; + + ScrollView *frogpilotPanel; +}; diff --git a/frogpilot/ui/qt/offroad/lateral_settings.cc b/frogpilot/ui/qt/offroad/lateral_settings.cc new file mode 100644 index 000000000..2430e46f2 --- /dev/null +++ b/frogpilot/ui/qt/offroad/lateral_settings.cc @@ -0,0 +1,431 @@ +#include "frogpilot/ui/qt/offroad/lateral_settings.h" + +FrogPilotLateralPanel::FrogPilotLateralPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *lateralLayout = new QStackedLayout(); + addItem(lateralLayout); + + FrogPilotListWidget *lateralList = new FrogPilotListWidget(this); + + ScrollView *lateralPanel = new ScrollView(lateralList, this); + + lateralLayout->addWidget(lateralPanel); + + FrogPilotListWidget *advancedLateralTuneList = new FrogPilotListWidget(this); + FrogPilotListWidget *aolList = new FrogPilotListWidget(this); + FrogPilotListWidget *laneChangeList = new FrogPilotListWidget(this); + FrogPilotListWidget *lateralTuneList = new FrogPilotListWidget(this); + FrogPilotListWidget *qolList = new FrogPilotListWidget(this); + + ScrollView *advancedLateralTunePanel = new ScrollView(advancedLateralTuneList, this); + ScrollView *aolPanel = new ScrollView(aolList, this); + ScrollView *laneChangePanel = new ScrollView(laneChangeList, this); + ScrollView *lateralTunePanel = new ScrollView(lateralTuneList, this); + ScrollView *qolPanel = new ScrollView(qolList, this); + + lateralLayout->addWidget(advancedLateralTunePanel); + lateralLayout->addWidget(aolPanel); + lateralLayout->addWidget(laneChangePanel); + lateralLayout->addWidget(lateralTunePanel); + lateralLayout->addWidget(qolPanel); + + const std::vector> lateralToggles { + {"AdvancedLateralTune", tr("Advanced Lateral Tuning"), tr("Advanced steering control changes to fine-tune how openpilot drives."), "../../frogpilot/assets/toggle_icons/icon_advanced_lateral_tune.png"}, + {"SteerDelay", parent->steerActuatorDelay != 0 ? QString(tr("Actuator Delay (Default: %1)")).arg(QString::number(parent->steerActuatorDelay, 'f', 2)) : tr("Actuator Delay"), tr("The time between openpilot's steering command and the vehicle's response. Increase if the vehicle reacts late; decrease if it feels jumpy. Auto-learned by default."), ""}, + {"SteerFriction", parent->friction != 0 ? QString(tr("Friction (Default: %1)")).arg(QString::number(parent->friction, 'f', 2)) : tr("Friction"), tr("Compensates for steering friction. Increase if the wheel sticks near center; decrease if it jitters. Auto-learned by default."), ""}, + {"SteerKP", parent->steerKp != 0 ? QString(tr("Kp Factor (Default: %1)")).arg(QString::number(parent->steerKp, 'f', 2)) : tr("Kp Factor"), tr("How strongly openpilot corrects lane position. Higher is tighter but twitchier; lower is smoother but slower. Auto-learned by default."), ""}, + {"SteerLatAccel", parent->latAccelFactor != 0 ? QString(tr("Lateral Acceleration (Default: %1)")).arg(QString::number(parent->latAccelFactor, 'f', 2)) : tr("Lateral Acceleration"), tr("Maps steering torque to turning response. Increase for sharper turns; decrease for gentler steering. Auto-learned by default."), ""}, + {"SteerRatio", parent->steerRatio != 0 ? QString(tr("Steer Ratio (Default: %1)")).arg(QString::number(parent->steerRatio, 'f', 2)) : tr("Steer Ratio"), tr("The relationship between steering wheel rotation and road wheel angle. Increase if steering feels too quick or twitchy; decrease if it feels too slow or weak. Auto-learned by default."), ""}, + {"ForceAutoTune", tr("Force Auto-Tune On"), tr("Force-enable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\"."), ""}, + {"ForceAutoTuneOff", tr("Force Auto-Tune Off"), tr("Force-disable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\" and use the set value instead."), ""}, + {"ForceTorqueController", tr("Force Torque Controller"), tr("Use torque-based steering control instead of angle-based control for smoother lane keeping, especially in curves."), ""}, + + {"AlwaysOnLateral", tr("Always On Lateral"), tr("openpilot's steering remains active even when the accelerator or brake pedals are pressed."), "../../frogpilot/assets/toggle_icons/icon_always_on_lateral.png"}, + {"AlwaysOnLateralLKAS", tr("Enable With LKAS"), tr("Enable \"Always On Lateral\" whenever \"LKAS\" is on, even when openpilot is not engaged."), ""}, + {"PauseAOLOnBrake", tr("Pause on Brake Press Below"), tr("Pause \"Always On Lateral\" below the set speed while the brake pedal is pressed."), ""}, + + {"LaneChanges", tr("Lane Changes"), tr("Allow openpilot to change lanes."), "../../frogpilot/assets/toggle_icons/icon_lane.png"}, + {"NudgelessLaneChange", tr("Automatic Lane Changes"), tr("When the turn signal is on, openpilot will automatically change lanes. No steering-wheel nudge required!"), ""}, + {"LaneChangeTime", tr("Lane Change Delay"), tr("Delay between turn signal activation and the start of an automatic lane change."), ""}, + {"MinimumLaneChangeSpeed", tr("Minimum Lane Change Speed"), tr("Lowest speed at which openpilot will change lanes."), ""}, + {"LaneDetectionWidth", tr("Minimum Lane Width"), tr("Prevent automatic lane changes into lanes narrower than the set width."), ""}, + {"OneLaneChange", tr("One Lane Change Per Signal"), tr("Limit automatic lane changes to one per turn-signal activation."), ""}, + + {"LateralTune", tr("Lateral Tuning"), tr("Miscellaneous steering control changes to fine-tune how openpilot drives."), "../../frogpilot/assets/toggle_icons/icon_lateral_tune.png"}, + {"TurnDesires", tr("Force Turn Desires Below Lane Change Speed"), tr("While driving below the minimum lane change speed with an active turn signal, instruct openpilot to turn left/right."), ""}, + {"NNFF", tr("Neural Network Feedforward (NNFF)"), tr("Twilsonco's \"Neural Network FeedForward\" controller. Uses a trained neural network model to predict steering torque based on vehicle speed, roll, and past/future planned path data for smoother, model-based steering."), ""}, + {"NNFFLite", tr("Neural Network Feedforward (NNFF) Lite"), tr("A lightweight version of Twilsonco's \"Neural Network FeedForward\" controller. Uses the \"look-ahead\" planned lateral jerk logic from the full model to help smoothen steering adjustments in curves, but does not use the full neural network for torque calculation."), ""}, + + {"QOLLateral", tr("Quality of Life"), tr("Steering control changes to fine-tune how openpilot drives."), "../../frogpilot/assets/toggle_icons/icon_quality_of_life.png"}, + {"PauseLateralSpeed", tr("Pause Steering Below"), tr("Pause steering below the set speed."), ""}, + + {"IgnoreMe", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, + {"IgnoreMe2", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""} + }; + + for (const auto &[param, title, desc, icon] : lateralToggles) { + AbstractControl *lateralToggle; + + if (param == "AdvancedLateralTune") { + FrogPilotManageControl *advancedLateralTuneToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(advancedLateralTuneToggle, &FrogPilotManageControl::manageButtonClicked, [lateralLayout, advancedLateralTunePanel]() { + lateralLayout->setCurrentWidget(advancedLateralTunePanel); + }); + lateralToggle = advancedLateralTuneToggle; + } else if (param == "SteerDelay") { + std::vector steerDelayButton{"Reset"}; + lateralToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 0.01, 1, QString(), std::map(), 0.01, false, {}, steerDelayButton, false, false); + } else if (param == "SteerFriction") { + std::vector steerFrictionButton{"Reset"}; + lateralToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 0, 1, QString(), std::map(), 0.01, false, {}, steerFrictionButton, false, false); + } else if (param == "SteerKP") { + std::vector steerKPButton{"Reset"}; + lateralToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, parent->steerKp * 0.5, parent->steerKp * 1.5, QString(), std::map(), 0.01, false, {}, steerKPButton, false, false); + } else if (param == "SteerLatAccel") { + std::vector steerLatAccelButton{"Reset"}; + lateralToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, parent->latAccelFactor * 0.5, parent->latAccelFactor * 1.5, QString(), std::map(), 0.01, false, {}, steerLatAccelButton, false, false); + } else if (param == "SteerRatio") { + std::vector steerRatioButton{"Reset"}; + lateralToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, parent->steerRatio * 0.5, parent->steerRatio * 1.5, QString(), std::map(), 0.01, false, {}, steerRatioButton, false, false); + + } else if (param == "AlwaysOnLateral") { + FrogPilotManageControl *aolToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(aolToggle, &FrogPilotManageControl::manageButtonClicked, [lateralLayout, aolPanel]() { + lateralLayout->setCurrentWidget(aolPanel); + }); + lateralToggle = aolToggle; + } else if (param == "PauseAOLOnBrake") { + lateralToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, QString(), std::map(), 1, true); + + } else if (param == "LaneChanges") { + FrogPilotManageControl *laneChangeToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(laneChangeToggle, &FrogPilotManageControl::manageButtonClicked, [lateralLayout, laneChangePanel]() { + lateralLayout->setCurrentWidget(laneChangePanel); + }); + lateralToggle = laneChangeToggle; + } else if (param == "LaneChangeTime") { + std::map laneChangeTimeLabels; + for (float i = 0; i <= 5; i += 0.1) { + laneChangeTimeLabels[i] = i == 0 ? tr("Instant") : std::lround(i / 0.1) == 1 / 0.1 ? QString::number(i, 'f', 1) + tr(" second") : QString::number(i, 'f', 1) + tr(" seconds"); + } + lateralToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 5, QString(), laneChangeTimeLabels, 0.1); + } else if (param == "LaneDetectionWidth") { + lateralToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 15, QString(), std::map(), 0.1, true); + } else if (param == "MinimumLaneChangeSpeed") { + lateralToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, QString(), std::map(), 1, true); + + } else if (param == "LateralTune") { + FrogPilotManageControl *lateralTuneToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(lateralTuneToggle, &FrogPilotManageControl::manageButtonClicked, [lateralLayout, lateralTunePanel]() { + lateralLayout->setCurrentWidget(lateralTunePanel); + }); + lateralToggle = lateralTuneToggle; + + } else if (param == "QOLLateral") { + FrogPilotManageControl *qolLateralToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(qolLateralToggle, &FrogPilotManageControl::manageButtonClicked, [lateralLayout, qolPanel]() { + lateralLayout->setCurrentWidget(qolPanel); + }); + lateralToggle = qolLateralToggle; + } else if (param == "PauseLateralSpeed") { + std::vector pauseLateralToggles{"PauseLateralOnSignal"}; + std::vector pauseLateralToggleNames{tr("Turn Signal Only")}; + lateralToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 0, 99, QString(), std::map(), 1, true, pauseLateralToggles, pauseLateralToggleNames, true); + + } else { + lateralToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = lateralToggle; + + if (advancedLateralTuneKeys.contains(param)) { + advancedLateralTuneList->addItem(lateralToggle); + } else if (aolKeys.contains(param)) { + aolList->addItem(lateralToggle); + } else if (laneChangeKeys.contains(param)) { + laneChangeList->addItem(lateralToggle); + } else if (lateralTuneKeys.contains(param)) { + lateralTuneList->addItem(lateralToggle); + } else if (qolKeys.contains(param)) { + qolList ->addItem(lateralToggle); + } else { + lateralList->addItem(lateralToggle); + + parentKeys.insert(param); + } + + if (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(lateralToggle)) { + QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + emit openSubPanel(); + openDescriptions(forceOpenDescriptions, toggles); + }); + } + + QObject::connect(lateralToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(lateralToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + QSet forceUpdateKeys = {"ForceAutoTune", "ForceAutoTuneOff", "LateralTune", "NNFF", "NudgelessLaneChange"}; + for (const QString &key : forceUpdateKeys) { + QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, this, &FrogPilotLateralPanel::updateToggles); + } + + QSet rebootKeys = {"AlwaysOnLateral", "ForceTorqueController", "NNFF", "NNFFLite"}; + for (const QString &key : rebootKeys) { + QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, [key, this](bool state) { + if (started) { + if (key == "AlwaysOnLateral" && state) { + if (FrogPilotConfirmationDialog::toggleReboot(this)) { + Hardware::reboot(); + } + } else if (key != "AlwaysOnLateral") { + if (FrogPilotConfirmationDialog::toggleReboot(this)) { + Hardware::reboot(); + } + } + } + }); + } + + steerDelayToggle = static_cast(toggles["SteerDelay"]); + QObject::connect(steerDelayToggle, &FrogPilotParamValueButtonControl::buttonClicked, [parent, this]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Reset Actuator Delay to its default value?"), this)) { + params.putFloat("SteerDelay", parent->steerActuatorDelay); + steerDelayToggle->refresh(); + } + }); + + steerFrictionToggle = static_cast(toggles["SteerFriction"]); + QObject::connect(steerFrictionToggle, &FrogPilotParamValueButtonControl::buttonClicked, [parent, this]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Reset Friction to its default value?"), this)) { + params.putFloat("SteerFriction", parent->friction); + steerFrictionToggle->refresh(); + } + }); + + steerKPToggle = static_cast(toggles["SteerKP"]); + QObject::connect(steerKPToggle, &FrogPilotParamValueButtonControl::buttonClicked, [parent, this]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Reset Kp Factor to its default value?"), this)) { + params.putFloat("SteerKP", parent->steerKp); + steerKPToggle->refresh(); + } + }); + + steerLatAccelToggle = static_cast(toggles["SteerLatAccel"]); + QObject::connect(steerLatAccelToggle, &FrogPilotParamValueButtonControl::buttonClicked, [parent, this]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Reset Lateral Accel to its default value?"), this)) { + params.putFloat("SteerLatAccel", parent->latAccelFactor); + steerLatAccelToggle->refresh(); + } + }); + + steerRatioToggle = static_cast(toggles["SteerRatio"]); + QObject::connect(steerRatioToggle, &FrogPilotParamValueButtonControl::buttonClicked, [parent, this]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Reset Steer Ratio to its default value?"), this)) { + params.putFloat("SteerRatio", parent->steerRatio); + steerRatioToggle->refresh(); + } + }); + + openDescriptions(forceOpenDescriptions, toggles); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [lateralLayout, lateralPanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + lateralLayout->setCurrentWidget(lateralPanel); + }); + QObject::connect(parent, &FrogPilotSettingsWindow::updateMetric, this, &FrogPilotLateralPanel::updateMetric); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotLateralPanel::updateState); +} + +void FrogPilotLateralPanel::showEvent(QShowEvent *event) { + steerDelayToggle->setTitle(QString(tr("Actuator Delay (Default: %1)")).arg(QString::number(parent->steerActuatorDelay, 'f', 2))); + steerFrictionToggle->setTitle(QString(tr("Friction (Default: %1)")).arg(QString::number(parent->friction, 'f', 2))); + steerKPToggle->setTitle(QString(tr("Kp Factor (Default: %1)")).arg(QString::number(parent->steerKp, 'f', 2))); + steerKPToggle->updateControl(parent->steerKp * 0.5, parent->steerKp * 1.5); + steerLatAccelToggle->setTitle(QString(tr("Lateral Accel (Default: %1)")).arg(QString::number(parent->latAccelFactor, 'f', 2))); + steerLatAccelToggle->updateControl(parent->latAccelFactor * 0.5, parent->latAccelFactor * 1.5); + steerRatioToggle->setTitle(QString(tr("Steer Ratio (Default: %1)")).arg(QString::number(parent->steerRatio, 'f', 2))); + steerRatioToggle->updateControl(parent->steerRatio * 0.5, parent->steerRatio * 1.5); + + updateToggles(); +} + +void FrogPilotLateralPanel::updateState(const UIState &s) { + if (!isVisible()) return; + + started = s.scene.started; +} + +void FrogPilotLateralPanel::updateMetric(bool metric, bool bootRun) { + static bool previousMetric; + if (metric != previousMetric && !bootRun) { + double distanceConversion = metric ? FOOT_TO_METER : METER_TO_FOOT; + double speedConversion = metric ? MILE_TO_KM : KM_TO_MILE; + + params.putFloatNonBlocking("LaneDetectionWidth", params.getFloat("LaneDetectionWidth") * distanceConversion); + + params.putIntNonBlocking("MinimumLaneChangeSpeed", params.getInt("MinimumLaneChangeSpeed") * speedConversion); + params.putIntNonBlocking("PauseAOLOnBrake", params.getInt("PauseAOLOnBrake") * speedConversion); + params.putIntNonBlocking("PauseLateralSpeed", params.getInt("PauseLateralSpeed") * speedConversion); + } + previousMetric = metric; + + static std::map imperialDistanceLabels; + static std::map imperialSpeedLabels; + static std::map metricDistanceLabels; + static std::map metricSpeedLabels; + + static bool labelsInitialized = false; + if (!labelsInitialized) { + for (int i = 0; i <= 150; ++i) { + float key = i / 10.0f; + imperialDistanceLabels[key] = key == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" foot") : QString::number(key, 'f', 1) + tr(" feet"); + } + + for (int i = 0; i <= 99; ++i) { + imperialSpeedLabels[i] = i == 0 ? tr("Off") : QString::number(i) + tr(" mph"); + } + + for (int i = 0; i <= 50; ++i) { + float key = i / 10.0f; + metricDistanceLabels[key] = key == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" meter") : QString::number(key, 'f', 1) + tr(" meters"); + } + + for (int i = 0; i <= 150; ++i) { + metricSpeedLabels[i] = i == 0 ? tr("Off") : QString::number(i) + tr(" km/h"); + } + + labelsInitialized = true; + } + + FrogPilotParamValueControl *laneWidthToggle = static_cast(toggles["LaneDetectionWidth"]); + FrogPilotParamValueControl *minimumLaneChangeSpeedToggle = static_cast(toggles["MinimumLaneChangeSpeed"]); + FrogPilotParamValueControl *pauseAOLOnBrakeToggle = static_cast(toggles["PauseAOLOnBrake"]); + FrogPilotParamValueControl *pauseLateralToggle = static_cast(toggles["PauseLateralSpeed"]); + + if (metric) { + laneWidthToggle->updateControl(0, 5, metricDistanceLabels); + + minimumLaneChangeSpeedToggle->updateControl(0, 150, metricSpeedLabels); + pauseAOLOnBrakeToggle->updateControl(0, 150, metricSpeedLabels); + pauseLateralToggle->updateControl(0, 150, metricSpeedLabels); + } else { + laneWidthToggle->updateControl(0, 15, imperialDistanceLabels); + + minimumLaneChangeSpeedToggle->updateControl(0, 99, imperialSpeedLabels); + pauseAOLOnBrakeToggle->updateControl(0, 99, imperialSpeedLabels); + pauseLateralToggle->updateControl(0, 99, imperialSpeedLabels); + } +} + +void FrogPilotLateralPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + bool forcingAutoTune = !parent->hasAutoTune && params.getBool("ForceAutoTune"); + bool forcingAutoTuneOff = parent->hasAutoTune && params.getBool("ForceAutoTuneOff"); + bool forcingTorqueController = !parent->isAngleCar && params.getBool("ForceTorqueController"); + bool usingNNFF = parent->hasNNFFLog && params.getBool("LateralTune") && params.getBool("NNFF"); + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "AlwaysOnLateralLKAS") { + setVisible &= parent->lkasAllowedForAOL; + } + + else if (key == "ForceAutoTune") { + setVisible &= !parent->hasAutoTune; + setVisible &= !parent->isAngleCar; + setVisible &= parent->isTorqueCar || forcingTorqueController || usingNNFF; + } + + else if (key == "ForceAutoTuneOff") { + setVisible &= parent->hasAutoTune; + } + + else if (key == "ForceTorqueController") { + setVisible &= !parent->isAngleCar; + setVisible &= !parent->isTorqueCar; + } + + else if (key == "LaneChangeTime") { + setVisible &= params.getBool("LaneChanges") && params.getBool("NudgelessLaneChange"); + } + + else if (key == "LaneDetectionWidth") { + setVisible &= params.getBool("LaneChanges") && params.getBool("NudgelessLaneChange"); + } + + else if (key == "NNFF") { + setVisible &= parent->hasNNFFLog; + setVisible &= !parent->isAngleCar; + } + + else if (key == "NNFFLite") { + setVisible &= !usingNNFF; + setVisible &= !parent->isAngleCar; + } + + else if (key == "SteerDelay") { + setVisible &= parent->steerActuatorDelay != 0; + } + + else if (key == "SteerFriction") { + setVisible &= parent->friction != 0; + setVisible &= parent->hasAutoTune ? forcingAutoTuneOff : !forcingAutoTune; + setVisible &= parent->isTorqueCar || forcingTorqueController || usingNNFF; + setVisible &= !usingNNFF; + } + + else if (key == "SteerKP") { + setVisible &= parent->steerKp != 0; + setVisible &= parent->isTorqueCar || forcingTorqueController || usingNNFF; + setVisible &= !parent->isAngleCar; + } + + else if (key == "SteerLatAccel") { + setVisible &= parent->latAccelFactor != 0; + setVisible &= parent->hasAutoTune ? forcingAutoTuneOff : !forcingAutoTune; + setVisible &= parent->isTorqueCar || forcingTorqueController || usingNNFF; + setVisible &= !usingNNFF; + } + + else if (key == "SteerRatio") { + setVisible &= parent->steerRatio != 0; + setVisible &= parent->hasAutoTune ? forcingAutoTuneOff : !forcingAutoTune; + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (advancedLateralTuneKeys.contains(key)) { + toggles["AdvancedLateralTune"]->setVisible(true); + } else if (aolKeys.contains(key)) { + toggles["AlwaysOnLateral"]->setVisible(true); + } else if (laneChangeKeys.contains(key)) { + toggles["LaneChanges"]->setVisible(true); + } else if (lateralTuneKeys.contains(key)) { + toggles["LateralTune"]->setVisible(true); + } else if (qolKeys.contains(key)) { + toggles["QOLLateral"]->setVisible(true); + } + } + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/lateral_settings.h b/frogpilot/ui/qt/offroad/lateral_settings.h new file mode 100644 index 000000000..08117e377 --- /dev/null +++ b/frogpilot/ui/qt/offroad/lateral_settings.h @@ -0,0 +1,44 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotLateralPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotLateralPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateMetric(bool metric, bool bootRun); + void updateState(const UIState &s); + void updateToggles(); + + bool forceOpenDescriptions; + bool started; + + std::map toggles; + + QSet advancedLateralTuneKeys = {"ForceAutoTune", "ForceAutoTuneOff", "ForceTorqueController", "SteerDelay", "SteerFriction", "SteerLatAccel", "SteerKP", "SteerRatio"}; + QSet aolKeys = {"AlwaysOnLateralLKAS", "PauseAOLOnBrake"}; + QSet laneChangeKeys = {"LaneChangeTime", "LaneDetectionWidth", "MinimumLaneChangeSpeed", "NudgelessLaneChange", "OneLaneChange"}; + QSet lateralTuneKeys = {"NNFF", "NNFFLite", "TurnDesires"}; + QSet qolKeys = {"PauseLateralSpeed"}; + + QSet parentKeys; + + FrogPilotParamValueButtonControl *steerDelayToggle; + FrogPilotParamValueButtonControl *steerFrictionToggle; + FrogPilotParamValueButtonControl *steerLatAccelToggle; + FrogPilotParamValueButtonControl *steerKPToggle; + FrogPilotParamValueButtonControl *steerRatioToggle; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/longitudinal_settings.cc b/frogpilot/ui/qt/offroad/longitudinal_settings.cc new file mode 100644 index 000000000..354e4bdaf --- /dev/null +++ b/frogpilot/ui/qt/offroad/longitudinal_settings.cc @@ -0,0 +1,1085 @@ +#include "frogpilot/ui/qt/offroad/longitudinal_settings.h" + +FrogPilotLongitudinalPanel::FrogPilotLongitudinalPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + networkManager = new QNetworkAccessManager(this); + + QStackedLayout *longitudinalLayout = new QStackedLayout(); + addItem(longitudinalLayout); + + FrogPilotListWidget *longitudinalList = new FrogPilotListWidget(this); + + ScrollView *longitudinalPanel = new ScrollView(longitudinalList, this); + + longitudinalLayout->addWidget(longitudinalPanel); + + FrogPilotListWidget *advancedLongitudinalTuneList = new FrogPilotListWidget(this); + FrogPilotListWidget *aggressivePersonalityList = new FrogPilotListWidget(this); + FrogPilotListWidget *conditionalExperimentalList = new FrogPilotListWidget(this); + FrogPilotListWidget *curveSpeedList = new FrogPilotListWidget(this); + FrogPilotListWidget *customDrivingPersonalityList = new FrogPilotListWidget(this); + FrogPilotListWidget *longitudinalTuneList = new FrogPilotListWidget(this); + FrogPilotListWidget *qolList = new FrogPilotListWidget(this); + FrogPilotListWidget *relaxedPersonalityList = new FrogPilotListWidget(this); + FrogPilotListWidget *speedLimitControllerList = new FrogPilotListWidget(this); + FrogPilotListWidget *speedLimitControllerOffsetsList = new FrogPilotListWidget(this); + FrogPilotListWidget *speedLimitControllerQOLList = new FrogPilotListWidget(this); + FrogPilotListWidget *speedLimitControllerVisualList = new FrogPilotListWidget(this); + FrogPilotListWidget *standardPersonalityList = new FrogPilotListWidget(this); + FrogPilotListWidget *trafficPersonalityList = new FrogPilotListWidget(this); + FrogPilotListWidget *weatherList = new FrogPilotListWidget(this); + FrogPilotListWidget *weatherLowVisibilityList = new FrogPilotListWidget(this); + FrogPilotListWidget *weatherRainList = new FrogPilotListWidget(this); + FrogPilotListWidget *weatherRainStormList = new FrogPilotListWidget(this); + FrogPilotListWidget *weatherSnowList = new FrogPilotListWidget(this); + + ScrollView *advancedLongitudinalTunePanel = new ScrollView(advancedLongitudinalTuneList, this); + ScrollView *aggressivePersonalityPanel = new ScrollView(aggressivePersonalityList, this); + ScrollView *conditionalExperimentalPanel = new ScrollView(conditionalExperimentalList, this); + ScrollView *curveSpeedPanel = new ScrollView(curveSpeedList, this); + ScrollView *customDrivingPersonalityPanel = new ScrollView(customDrivingPersonalityList, this); + ScrollView *longitudinalTunePanel = new ScrollView(longitudinalTuneList, this); + ScrollView *qolPanel = new ScrollView(qolList, this); + ScrollView *relaxedPersonalityPanel = new ScrollView(relaxedPersonalityList, this); + ScrollView *speedLimitControllerPanel = new ScrollView(speedLimitControllerList, this); + ScrollView *speedLimitControllerOffsetsPanel = new ScrollView(speedLimitControllerOffsetsList, this); + ScrollView *speedLimitControllerQOLPanel = new ScrollView(speedLimitControllerQOLList, this); + ScrollView *speedLimitControllerVisualPanel = new ScrollView(speedLimitControllerVisualList, this); + ScrollView *standardPersonalityPanel = new ScrollView(standardPersonalityList, this); + ScrollView *trafficPersonalityPanel = new ScrollView(trafficPersonalityList, this); + ScrollView *weatherLowVisibilityPanel = new ScrollView(weatherLowVisibilityList, this); + ScrollView *weatherPanel = new ScrollView(weatherList, this); + ScrollView *weatherRainPanel = new ScrollView(weatherRainList, this); + ScrollView *weatherRainStormPanel = new ScrollView(weatherRainStormList, this); + ScrollView *weatherSnowPanel = new ScrollView(weatherSnowList, this); + + longitudinalLayout->addWidget(advancedLongitudinalTunePanel); + longitudinalLayout->addWidget(aggressivePersonalityPanel); + longitudinalLayout->addWidget(conditionalExperimentalPanel); + longitudinalLayout->addWidget(curveSpeedPanel); + longitudinalLayout->addWidget(customDrivingPersonalityPanel); + longitudinalLayout->addWidget(longitudinalTunePanel); + longitudinalLayout->addWidget(qolPanel); + longitudinalLayout->addWidget(relaxedPersonalityPanel); + longitudinalLayout->addWidget(speedLimitControllerPanel); + longitudinalLayout->addWidget(speedLimitControllerOffsetsPanel); + longitudinalLayout->addWidget(speedLimitControllerQOLPanel); + longitudinalLayout->addWidget(speedLimitControllerVisualPanel); + longitudinalLayout->addWidget(standardPersonalityPanel); + longitudinalLayout->addWidget(trafficPersonalityPanel); + longitudinalLayout->addWidget(weatherLowVisibilityPanel); + longitudinalLayout->addWidget(weatherPanel); + longitudinalLayout->addWidget(weatherRainPanel); + longitudinalLayout->addWidget(weatherRainStormPanel); + longitudinalLayout->addWidget(weatherSnowPanel); + + const std::vector> longitudinalToggles { + {"AdvancedLongitudinalTune", tr("Advanced Longitudinal Tuning"), tr("Advanced acceleration and braking control changes to fine-tune how openpilot drives."), "../../frogpilot/assets/toggle_icons/icon_advanced_longitudinal_tune.png"}, + {"LongitudinalActuatorDelay", parent->longitudinalActuatorDelay != 0 ? QString(tr("Actuator Delay (Default: %1)")).arg(QString::number(parent->longitudinalActuatorDelay, 'f', 2)) : tr("Actuator Delay"), tr("The time between openpilot's throttle or brake command and the vehicle's response. Increase if the vehicle feels slow to react; decrease if it feels too eager or overshoots."), ""}, + {"MaxDesiredAcceleration", tr("Maximum Acceleration"), tr("Limit the strongest acceleration openpilot can command."), ""}, + {"StartAccel", parent->startAccel != 0 ? QString(tr("Start Acceleration (Default: %1)")).arg(QString::number(parent->startAccel, 'f', 2)) : tr("Start Acceleration"), tr("Extra acceleration applied when starting from a stop. Increase for quicker takeoffs; decrease for smoother, gentler starts."), ""}, + {"VEgoStarting", parent->vEgoStarting != 0 ? QString(tr("Start Speed (Default: %1)")).arg(QString::number(parent->vEgoStarting, 'f', 2)) : tr("Start Speed"), tr("The speed at which openpilot exits the stopped state. Increase to reduce creeping; decrease to move sooner after stopping."), ""}, + {"StopAccel", parent->stopAccel != 0 ? QString(tr("Stop Acceleration (Default: %1)")).arg(QString::number(parent->stopAccel, 'f', 2)) : tr("Stop Acceleration"), tr("Brake force applied to hold the vehicle at a standstill. Increase to prevent rolling on hills; decrease for smoother, softer stops."), ""}, + {"StoppingDecelRate", parent->stoppingDecelRate != 0 ? QString(tr("Stopping Rate (Default: %1)")).arg(QString::number(parent->stoppingDecelRate, 'f', 2)) : tr("Stopping Rate"), tr("How quickly braking ramps up when stopping. Increase for shorter, firmer stops; decrease for smoother, longer stops."), ""}, + {"VEgoStopping", parent->vEgoStopping != 0 ? QString(tr("Stop Speed (Default: %1)")).arg(QString::number(parent->vEgoStopping, 'f', 2)) : tr("Stop Speed"), tr("The speed at which openpilot considers the vehicle stopped. Increase to brake earlier and stop smoothly; decrease to wait longer but risk overshooting."), ""}, + + {"ConditionalExperimental", tr("Conditional Experimental Mode"), tr("Automatically switch to \"Experimental Mode\" when set conditions are met. Allows the model to handle challenging situations with smarter decision making."), "../../frogpilot/assets/toggle_icons/icon_conditional.png"}, + {"CESpeed", tr("Below"), tr("Switch to \"Experimental Mode\" when driving below this speed without a lead to help openpilot handle low-speed situations more smoothly."), ""}, + {"CECurves", tr("Curve Detected Ahead"), tr("Switch to \"Experimental Mode\" when a curve is detected to allow the model to set an appropriate speed for the curve."), ""}, + {"CEStopLights", tr("\"Detected\" Stop Lights/Signs"), tr("Switch to \"Experimental Mode\" whenever the driving model \"detects\" a red light or stop sign.

Disclaimer: openpilot does not explicitly detect traffic lights or stop signs. In \"Experimental Mode\", openpilot makes end-to-end driving decisions from camera input, which means it may stop even when there's no clear reason!"), ""}, + {"CELead", tr("Lead Detected Ahead"), tr("Switch to \"Experimental Mode\" when a slower or stopped vehicle is detected. Can make braking smoother and more reliable on some vehicles."), ""}, + {"CEModelStopTime", tr("Predicted Stop In"), tr("Switch to \"Experimental Mode\" when openpilot predicts a stop within the set time. This is usually triggered when the model \"sees\" a red light or stop sign ahead.

Disclaimer: openpilot does not explicitly detect traffic lights or stop signs. In \"Experimental Mode\", openpilot makes end-to-end driving decisions from camera input, which means it may stop even when there's no clear reason!"), ""}, + {"CESignalSpeed", tr("Turn Signal Below"), tr("Switch to \"Experimental Mode\" when using a turn signal below the set speed to allow the model to choose an appropriate speed for smoother left and right turns."), ""}, + {"ShowCEMStatus", tr("Status Widget"), tr("Show which condition triggered \"Experimental Mode\" on the driving screen."), ""}, + + {"CurveSpeedController", tr("Curve Speed Controller"), tr("Automatically slow down for upcoming curves using data learned from your driving style, adapting to curves as you would."), "../../frogpilot/assets/toggle_icons/icon_speed_map.png"}, + {"CalibratedLateralAcceleration", tr("Calibrated Lateral Acceleration"), tr("The learned lateral acceleration from collected driving data. This sets how fast openpilot will take curves. Higher values allow faster cornering; lower values slow the vehicle for gentler turns."), ""}, + {"CalibrationProgress", tr("Calibration Progress"), tr("How much curve data has been collected. This is a progress meter; it is normal for the value to stay low and rarely reach 100%."), ""}, + {"ResetCurveData", tr("Reset Curve Data"), tr("Reset collected user data for \"Curve Speed Controller\"."), ""}, + {"ShowCSCStatus", tr("Status Widget"), tr("Show the \"Curve Speed Controller\" target speed on the driving screen."), ""}, + + {"CustomPersonalities", tr("Driving Personalities"), tr("Customize the \"Driving Personalities\" to better match your driving style."), "../../frogpilot/assets/toggle_icons/icon_personality.png"}, + + {"TrafficPersonalityProfile", tr("Traffic Mode"), tr("Customize the \"Traffic Mode\" personality profile. Designed for stop-and-go driving."), "../../frogpilot/assets/stock_theme/distance_icons/traffic.png"}, + {"TrafficFollow", tr("Following Distance"), tr("The minimum following distance to the lead vehicle in \"Traffic Mode\". openpilot blends between this value and the \"Aggressive\" profile as speed increases. Increase for more space; decrease for tighter gaps."), ""}, + {"TrafficJerkAcceleration", tr("Acceleration Smoothness"), tr("How smoothly openpilot accelerates in \"Traffic Mode\". Increase for gentler starts; decrease for faster but more abrupt takeoffs."), ""}, + {"TrafficJerkDeceleration", tr("Braking Smoothness"), tr("How smoothly openpilot brakes in \"Traffic Mode\". Increase for gentler stops; decrease for quicker but sharper braking."), ""}, + {"TrafficJerkDanger", tr("Safety Gap Bias"), tr("How much extra space openpilot keeps from the vehicle ahead in \"Traffic Mode\". Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following."), ""}, + {"TrafficJerkSpeedDecrease", tr("Slowdown Response"), tr("How smoothly openpilot slows down in \"Traffic Mode\". Increase for more gradual deceleration; decrease for faster but sharper slowdowns."), ""}, + {"TrafficJerkSpeed", tr("Speed-Up Response"), tr("How smoothly openpilot speeds up in \"Traffic Mode\". Increase for more gradual acceleration; decrease for quicker but more jolting acceleration."), ""}, + {"ResetTrafficPersonality", tr("Reset to Defaults"), tr("Reset \"Traffic Mode\" settings to defaults."), ""}, + + {"AggressivePersonalityProfile", tr("Aggressive"), tr("Customize the \"Aggressive\" personality profile. Designed for assertive driving with tighter gaps."), "../../frogpilot/assets/stock_theme/distance_icons/aggressive.png"}, + {"AggressiveFollow", tr("Following Distance"), tr("How many seconds openpilot follows behind lead vehicles when using the \"Aggressive\" profile. Increase for more space; decrease for tighter gaps.

Default: 1.25 seconds."), ""}, + {"AggressiveJerkAcceleration", tr("Acceleration Smoothness"), tr("How smoothly openpilot accelerates with the \"Aggressive\" profile. Increase for gentler starts; decrease for faster but more abrupt takeoffs."), ""}, + {"AggressiveJerkDeceleration", tr("Braking Smoothness"), tr("How smoothly openpilot brakes with the \"Aggressive\" profile. Increase for gentler stops; decrease for quicker but sharper braking."), ""}, + {"AggressiveJerkDanger", tr("Safety Gap Bias"), tr("How much extra space openpilot keeps from the vehicle ahead with the \"Aggressive\" profile. Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following."), ""}, + {"AggressiveJerkSpeedDecrease", tr("Slowdown Response"), tr("How smoothly openpilot slows down with the \"Aggressive\" profile. Increase for more gradual deceleration; decrease for faster but sharper slowdowns."), ""}, + {"AggressiveJerkSpeed", tr("Speed-Up Response"), tr("How smoothly openpilot speeds up with the \"Aggressive\" profile. Increase for more gradual acceleration; decrease for quicker but more jolting acceleration."), ""}, + {"ResetAggressivePersonality", tr("Reset to Defaults"), tr("Reset the \"Aggressive\" profile to defaults."), ""}, + + {"StandardPersonalityProfile", tr("Standard"), tr("Customize the \"Standard\" personality profile. Designed for balanced driving with moderate gaps."), "../../frogpilot/assets/stock_theme/distance_icons/standard.png"}, + {"StandardFollow", tr("Following Distance"), tr("How many seconds openpilot follows behind lead vehicles when using the \"Standard\" profile. Increase for more space; decrease for tighter gaps.

Default: 1.45 seconds."), ""}, + {"StandardJerkAcceleration", tr("Acceleration Smoothness"), tr("How smoothly openpilot accelerates with the \"Standard\" profile. Increase for gentler starts; decrease for faster but more abrupt takeoffs."), ""}, + {"StandardJerkDeceleration", tr("Braking Smoothness"), tr("How smoothly openpilot brakes with the \"Standard\" profile. Increase for gentler stops; decrease for quicker but sharper braking."), ""}, + {"StandardJerkDanger", tr("Safety Gap Bias"), tr("How much extra space openpilot keeps from the vehicle ahead with the \"Standard\" profile. Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following."), ""}, + {"StandardJerkSpeedDecrease", tr("Slowdown Response"), tr("How smoothly openpilot slows down with the \"Standard\" profile. Increase for more gradual deceleration; decrease for faster but sharper slowdowns."), ""}, + {"StandardJerkSpeed", tr("Speed-Up Response"), tr("How smoothly openpilot speeds up with the \"Standard\" profile. Increase for more gradual acceleration; decrease for quicker but more jolting acceleration."), ""}, + {"ResetStandardPersonality", tr("Reset to Defaults"), tr("Reset the \"Standard\" profile to defaults."), ""}, + + {"RelaxedPersonalityProfile", tr("Relaxed"), tr("Customize the \"Relaxed\" personality profile. Designed for smoother, more comfortable driving with larger gaps."), "../../frogpilot/assets/stock_theme/distance_icons/relaxed.png"}, + {"RelaxedFollow", tr("Following Distance"), tr("How many seconds openpilot follows behind lead vehicles when using the \"Relaxed\" profile. Increase for more space; decrease for tighter gaps.

Default: 1.75 seconds."), ""}, + {"RelaxedJerkAcceleration", tr("Acceleration Smoothness"), tr("How smoothly openpilot accelerates with the \"Relaxed\" profile. Increase for gentler starts; decrease for faster but more abrupt takeoffs."), ""}, + {"RelaxedJerkDeceleration", tr("Braking Smoothness"), tr("How smoothly openpilot brakes with the \"Relaxed\" profile. Increase for gentler stops; decrease for quicker but sharper braking."), ""}, + {"RelaxedJerkDanger", tr("Safety Gap Bias"), tr("How much extra space openpilot keeps from the vehicle ahead with the \"Relaxed\" profile. Increase for larger gaps and more cautious following; decrease for tighter gaps and closer following."), ""}, + {"RelaxedJerkSpeedDecrease", tr("Slowdown Response"), tr("How smoothly openpilot slows down with the \"Relaxed\" profile. Increase for more gradual deceleration; decrease for faster but sharper slowdowns."), ""}, + {"RelaxedJerkSpeed", tr("Speed-Up Response"), tr("How smoothly openpilot speeds up with the \"Relaxed\" profile. Increase for more gradual acceleration; decrease for quicker but more jolting acceleration."), ""}, + {"ResetRelaxedPersonality", tr("Reset to Defaults"), tr("Reset the \"Relaxed\" profile to defaults."), ""}, + + {"LongitudinalTune", tr("Longitudinal Tuning"), tr("Acceleration and braking control changes to fine-tune how openpilot drives."), "../../frogpilot/assets/toggle_icons/icon_longitudinal_tune.png"}, + {"AccelerationProfile", tr("Acceleration Profile"), tr("How quickly openpilot speeds up. \"Eco\" is gentle and efficient, \"Sport\" is firmer and more responsive, and \"Sport+\" accelerates at the maximum rate allowed."), ""}, + {"DecelerationProfile", tr("Deceleration Profile"), tr("How firmly openpilot slows down. \"Eco\" favors coasting, \"Sport\" applies stronger braking."), ""}, + {"HumanAcceleration", tr("Human-Like Acceleration"), tr("Acceleration that mimics human behavior by easing the throttle at low speeds and adding extra power when taking off from a stop."), ""}, + {"HumanFollowing", tr("Human-Like Following"), tr("Following behavior that mimics human drivers by closing gaps behind faster vehicles for quicker takeoffs and dynamically adjusting the desired following distance for gentler, more efficient braking."), ""}, + {"HumanLaneChanges", tr("Human-Like Lane Changes"), tr("Lane-change behavior that mimics human drivers by anticipating and tracking adjacent vehicles during lane changes."), ""}, + {"LeadDetectionThreshold", tr("Lead Detection Sensitivity"), tr("How sensitive openpilot is to detecting vehicles. Higher sensitivity allows quicker detection at longer distances but may react to non-vehicle objects; lower sensitivity is more conservative and reduces false detections."), ""}, + {"TacoTune", tr("\"Taco Bell Run\" Turn Speed Hack"), tr("The turn-speed hack from comma's 2022 \"Taco Bell Run\". Designed to slow down for left and right turns."), ""}, + + {"QOLLongitudinal", tr("Quality of Life"), tr("Miscellaneous acceleration and braking control changes to fine-tune how openpilot drives."), "../../frogpilot/assets/toggle_icons/icon_quality_of_life.png"}, + {"CustomCruise", tr("Cruise Interval"), tr("How much the set speed increases or decreases for each + or – cruise control button press."), ""}, + {"CustomCruiseLong", tr("Cruise Interval (Hold)"), tr("How much the set speed increases or decreases while holding the + or – cruise control buttons."), ""}, + {"ForceStops", tr("Force Stop at \"Detected\" Stop Lights/Signs"), tr("Force openpilot to stop whenever the driving model \"detects\" a red light or stop sign.

Disclaimer: openpilot does not explicitly detect traffic lights or stop signs. In \"Experimental Mode\", openpilot makes end-to-end driving decisions from camera input, which means it may stop even when there's no clear reason!"), ""}, + {"IncreasedStoppedDistance", tr("Increase Stopped Distance by:"), tr("Add extra space when stopped behind vehicles. Increase for more room; decrease for shorter gaps."), ""}, + {"MapGears", tr("Map Accel/Decel to Gears"), tr("Map the Acceleration or Deceleration profiles to the vehicle's \"Eco\" and \"Sport\" gear modes."), ""}, + {"SetSpeedOffset", tr("Offset Set Speed by:"), tr("Increase the set speed by the chosen offset. For example, set +5 if you usually drive 5 over the limit."), ""}, + {"ReverseCruise", tr("Reverse Cruise Increase"), tr("Reverse the cruise control button behavior so a short press increases the set speed by 5 instead of 1."), ""}, + {"WeatherPresets", tr("Weather Condition Offsets"), tr("Automatically adjust driving behavior based on real-time weather. Helps maintain comfort and safety in low visibility, rain, or snow."), ""}, + + {"LowVisibilityOffsets", tr("Low Visibility"), tr("Driving adjustments for fog, haze, or other low-visibility conditions."), ""}, + {"IncreaseFollowingLowVisibility", tr("Increase Following Distance by:"), tr("Add extra space behind lead vehicles in low visibility. Increase for more space; decrease for tighter gaps."), ""}, + {"IncreasedStoppedDistanceLowVisibility", tr("Increase Stopped Distance by:"), tr("Add extra buffer when stopped behind vehicles in low visibility. Increase for more room; decrease for shorter gaps."), ""}, + {"ReduceAccelerationLowVisibility", tr("Reduce Acceleration by:"), tr("Lower the maximum acceleration in low visibility. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."), ""}, + {"ReduceLateralAccelerationLowVisibility", tr("Reduce Speed in Curves by:"), tr("Lower the desired speed while driving through curves in low visibility. Increase for safer, gentler turns; decrease for more aggressive driving in curves."), ""}, + + {"RainOffsets", tr("Rain"), tr("Driving adjustments for rainy conditions."), ""}, + {"IncreaseFollowingRain", tr("Increase Following Distance by:"), tr("Add extra space behind lead vehicles in rain. Increase for more space; decrease for tighter gaps."), ""}, + {"IncreasedStoppedDistanceRain", tr("Increase Stopped Distance by:"), tr("Add extra buffer when stopped behind vehicles in rain. Increase for more room; decrease for shorter gaps."), ""}, + {"ReduceAccelerationRain", tr("Reduce Acceleration by:"), tr("Lower the maximum acceleration in rain. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."), ""}, + {"ReduceLateralAccelerationRain", tr("Reduce Speed in Curves by:"), tr("Lower the desired speed while driving through curves in rain. Increase for safer, gentler turns; decrease for more aggressive driving in curves."), ""}, + + {"RainStormOffsets", tr("Rainstorms"), tr("Driving adjustments for rainstorms."), ""}, + {"IncreaseFollowingRainStorm", tr("Increase Following Distance by:"), tr("Add extra space behind lead vehicles in a rainstorm. Increase for more space; decrease for tighter gaps."), ""}, + {"IncreasedStoppedDistanceRainStorm", tr("Increase Stopped Distance by:"), tr("Add extra buffer when stopped behind vehicles in a rainstorm. Increase for more room; decrease for shorter gaps."), ""}, + {"ReduceAccelerationRainStorm", tr("Reduce Acceleration by:"), tr("Lower the maximum acceleration in a rainstorm. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."), ""}, + {"ReduceLateralAccelerationRainStorm", tr("Reduce Speed in Curves by:"), tr("Lower the desired speed while driving through curves in a rainstorm. Increase for safer, gentler turns; decrease for more aggressive driving in curves."), ""}, + + {"SnowOffsets", tr("Snow"), tr("Driving adjustments for snowy conditions."), ""}, + {"IncreaseFollowingSnow", tr("Increase Following Distance by:"), tr("Add extra space behind lead vehicles in snow. Increase for more space; decrease for tighter gaps."), ""}, + {"IncreasedStoppedDistanceSnow", tr("Increase Stopped Distance by:"), tr("Add extra buffer when stopped behind vehicles in snow. Increase for more room; decrease for shorter gaps."), ""}, + {"ReduceAccelerationSnow", tr("Reduce Acceleration by:"), tr("Lower the maximum acceleration in snow. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."), ""}, + {"ReduceLateralAccelerationSnow", tr("Reduce Speed in Curves by:"), tr("Lower the desired speed while driving through curves in snow. Increase for safer, gentler turns; decrease for more aggressive driving in curves."), ""}, + + {"SetWeatherKey", tr("Set Your Own Key"), tr("Set your own \"OpenWeatherMap\" key to increase the weather update rate.

Personal keys grant 1,000 free calls per day, allowing for updates every minute. The default key is shared and only updates every 15 minutes."), ""}, + + {"SpeedLimitController", tr("Speed Limit Controller"), tr("Limit openpilot's maximum driving speed to the current speed limit obtained from downloaded maps, Mapbox, or the dashboard for supported vehicles (Ford, Genesis, Hyundai, Kia, Lexus, Toyota)."), "../../frogpilot/assets/toggle_icons/icon_speed_limit.png"}, + {"SLCFallback", tr("Fallback Speed"), tr("The speed used by \"Speed Limit Controller\" when no speed limit is found.

- Set Speed: Use the cruise set speed
- Experimental Mode: Estimate the limit using the driving model
- Previous Limit: Keep using the last confirmed limit"), ""}, + {"SLCOverride", tr("Override Speed"), tr("The speed used by \"Speed Limit Controller\" after you manually drive faster than the posted limit.

- Set with Gas Pedal: Use the highest speed reached while pressing the gas
- Max Set Speed: Use the cruise set speed

Overrides clear when openpilot disengages."), ""}, + {"SLCQOL", tr("Quality of Life"), tr("Miscellaneous \"Speed Limit Controller\" changes to fine-tune how openpilot drives."), ""}, + {"SLCConfirmation", tr("Confirm New Speed Limits"), tr("Ask before changing to a new speed limit. To accept, tap the flashing on-screen widget or press the Cruise Increase button. To deny, press the Cruise Decrease button or ignore the prompt for 30 seconds."), ""}, + {"SLCLookaheadHigher", tr("Higher Limit Lookahead Time"), tr("How far ahead openpilot anticipates upcoming higher speed limits from downloaded map data."), ""}, + {"SLCLookaheadLower", tr("Lower Limit Lookahead Time"), tr("How far ahead openpilot anticipates upcoming lower speed limits from downloaded map data."), ""}, + {"SetSpeedLimit", tr("Match Speed Limit on Engage"), tr("When openpilot is first enabled, automatically set the max speed to the current posted limit."), ""}, + {"SLCMapboxFiller", tr("Use Mapbox as Fallback"), tr("Use Mapbox speed-limit data when no other source is available."), ""}, + {"SLCPriority", tr("Speed Limit Source Priority"), tr("The source order for speed limits when more than one is available."), ""}, + {"SLCOffsets", tr("Speed Limit Offsets"), tr("Add an offset to the posted speed limit to better match your driving style."), ""}, + {"Offset1", tr("Speed Offset (0–24 mph)"), tr("How much to offset posted speed-limits between 0 and 24 mph."), ""}, + {"Offset2", tr("Speed Offset (25–34 mph)"), tr("How much to offset posted speed-limits between 25 and 34 mph."), ""}, + {"Offset3", tr("Speed Offset (35–44 mph)"), tr("How much to offset posted speed-limits between 35 and 44 mph."), ""}, + {"Offset4", tr("Speed Offset (45–54 mph)"), tr("How much to offset posted speed-limits between 45 and 54 mph."), ""}, + {"Offset5", tr("Speed Offset (55–64 mph)"), tr("How much to offset posted speed-limits between 55 and 64 mph."), ""}, + {"Offset6", tr("Speed Offset (65–74 mph)"), tr("How much to offset posted speed-limits between 65 and 74 mph."), ""}, + {"Offset7", tr("Speed Offset (75–99 mph)"), tr("How much to offset posted speed-limits between 75 and 99 mph."), ""}, + {"SLCVisuals", tr("Visual Settings"), tr("Visual \"Speed Limit Controller\" changes to fine-tune how the driving screen looks."), ""}, + {"ShowSLCOffset", tr("Show Speed Limit Offset"), tr("Show the current offset from the posted limit on the driving screen."), ""}, + {"SpeedLimitSources", tr("Show Speed Limit Sources"), tr("Display the speed-limit sources and their current values on the driving screen."), ""} + }; + + for (const auto &[param, title, desc, icon] : longitudinalToggles) { + AbstractControl *longitudinalToggle; + + if (param == "AdvancedLongitudinalTune") { + FrogPilotManageControl *advancedLongitudinalTuneToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(advancedLongitudinalTuneToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, advancedLongitudinalTunePanel]() { + longitudinalLayout->setCurrentWidget(advancedLongitudinalTunePanel); + }); + longitudinalToggle = advancedLongitudinalTuneToggle; + } else if (param == "LongitudinalActuatorDelay") { + longitudinalActuatorDelayToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 1, tr(" seconds"), std::map(), 0.01); + longitudinalToggle = longitudinalActuatorDelayToggle; + } else if (param == "MaxDesiredAcceleration") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0.1, 4.0, tr(" m/s²"), std::map(), 0.1); + } else if (param == "StartAccel") { + startAccelToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 4, tr(" m/s²"), std::map(), 0.01, true); + longitudinalToggle = startAccelToggle; + } else if (param == "VEgoStarting") { + vEgoStartingToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0.01, 1, tr(" m/s²"), std::map(), 0.01); + longitudinalToggle = vEgoStartingToggle; + } else if (param == "StopAccel") { + stopAccelToggle = new FrogPilotParamValueControl(param, title, desc, icon, -4, 0, tr(" m/s²"), std::map(), 0.01, true); + longitudinalToggle = stopAccelToggle; + } else if (param == "StoppingDecelRate") { + stoppingDecelRateToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0.001, 1, tr(" m/s²"), std::map(), 0.001, true); + longitudinalToggle = stoppingDecelRateToggle; + } else if (param == "VEgoStopping") { + vEgoStoppingToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0.01, 1, tr(" m/s²"), std::map(), 0.01); + longitudinalToggle = vEgoStoppingToggle; + + } else if (param == "ConditionalExperimental") { + FrogPilotManageControl *conditionalExperimentalToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(conditionalExperimentalToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, conditionalExperimentalPanel]() { + longitudinalLayout->setCurrentWidget(conditionalExperimentalPanel); + }); + longitudinalToggle = conditionalExperimentalToggle; + } else if (param == "CESpeed") { + FrogPilotParamValueControl *CESpeed = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, tr(" mph"), std::map(), 1, true, 175); + FrogPilotParamValueControl *CESpeedLead = new FrogPilotParamValueControl("CESpeedLead", tr("With Lead"), tr("Switch to \"Experimental Mode\" when driving below this speed with a lead to help openpilot handle low-speed situations more smoothly."), icon, 0, 99, tr(" mph"), std::map(), 1, true, 175); + FrogPilotDualParamValueControl *conditionalSpeeds = new FrogPilotDualParamValueControl(CESpeed, CESpeedLead); + longitudinalToggle = reinterpret_cast(conditionalSpeeds); + } else if (param == "CECurves") { + std::vector curveToggles{"CECurvesLead"}; + std::vector curveToggleNames{tr("With Lead")}; + longitudinalToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, curveToggles, curveToggleNames); + } else if (param == "CELead") { + std::vector leadToggles{"CESlowerLead", "CEStoppedLead"}; + std::vector leadToggleNames{tr("Slower Lead"), tr("Stopped Lead")}; + longitudinalToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, leadToggles, leadToggleNames); + } else if (param == "CEModelStopTime") { + std::map stopTimeLabels; + for (int i = 0; i <= 10; ++i) { + stopTimeLabels[i] = i == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" second") : QString::number(i) + tr(" seconds"); + } + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 9, QString(), stopTimeLabels); + } else if (param == "CESignalSpeed") { + std::vector ceSignalToggles{"CESignalLaneDetection"}; + std::vector ceSignalToggleNames{tr("Not For Detected Lanes")}; + longitudinalToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 0, 99, tr(" mph"), std::map(), 1.0, true, ceSignalToggles, ceSignalToggleNames, true); + + } else if (param == "CurveSpeedController") { + FrogPilotManageControl *curveControlToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(curveControlToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, curveSpeedPanel]() { + longitudinalLayout->setCurrentWidget(curveSpeedPanel); + }); + longitudinalToggle = curveControlToggle; + } else if (param == "CalibrationProgress") { + calibrationProgressLabel = new LabelControl(title, QString::number(params.getFloat("CalibrationProgress"), 'f', 2) + "%", desc); + longitudinalToggle = calibrationProgressLabel; + } else if (param == "CalibratedLateralAcceleration") { + calibratedLateralAccelerationLabel = new LabelControl(title, QString::number(params.getFloat("CalibratedLateralAcceleration"), 'f', 2) + tr(" m/s²"), desc); + longitudinalToggle = calibratedLateralAccelerationLabel; + } else if (param == "ResetCurveData") { + ButtonControl *resetCurveDataButton = new ButtonControl(title, tr("RESET"), desc); + QObject::connect(resetCurveDataButton, &ButtonControl::clicked, [this]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your curvature data?"), this)) { + params.putFloat("CalibratedLateralAcceleration", 2.00); + params.remove("CalibrationProgress"); + params.remove("CurvatureData"); + + calibratedLateralAccelerationLabel->setText(QString::number(2.00, 'f', 2) + tr(" m/s²")); + calibrationProgressLabel->setText(QString::number(0.00, 'f', 2) + "%"); + } + }); + longitudinalToggle = resetCurveDataButton; + + } else if (param == "CustomPersonalities") { + FrogPilotManageControl *customPersonalitiesToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(customPersonalitiesToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, customDrivingPersonalityPanel]() { + longitudinalLayout->setCurrentWidget(customDrivingPersonalityPanel); + }); + longitudinalToggle = customPersonalitiesToggle; + } else if (param == "ResetTrafficPersonality" || param == "ResetAggressivePersonality" || param == "ResetStandardPersonality" || param == "ResetRelaxedPersonality") { + ButtonControl *resetButton = new ButtonControl(title, tr("RESET"), desc); + longitudinalToggle = resetButton; + } else if (param == "TrafficPersonalityProfile") { + FrogPilotButtonsControl *trafficPersonalityToggle = new FrogPilotButtonsControl(title, desc, icon, {tr("MANAGE")}); + QObject::connect(trafficPersonalityToggle, &FrogPilotButtonsControl::buttonClicked, [longitudinalLayout, trafficPersonalityPanel, this](int id) { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(trafficPersonalityPanel); + + customPersonalityOpen = true; + }); + longitudinalToggle = trafficPersonalityToggle; + } else if (param == "AggressivePersonalityProfile") { + FrogPilotButtonsControl *aggressivePersonalityToggle = new FrogPilotButtonsControl(title, desc, icon, {tr("MANAGE")}); + QObject::connect(aggressivePersonalityToggle, &FrogPilotButtonsControl::buttonClicked, [longitudinalLayout, aggressivePersonalityPanel, this](int id) { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(aggressivePersonalityPanel); + + customPersonalityOpen = true; + }); + longitudinalToggle = aggressivePersonalityToggle; + } else if (param == "StandardPersonalityProfile") { + FrogPilotButtonsControl *standardPersonalityToggle = new FrogPilotButtonsControl(title, desc, icon, {tr("MANAGE")}); + QObject::connect(standardPersonalityToggle, &FrogPilotButtonsControl::buttonClicked, [longitudinalLayout, standardPersonalityPanel, this](int id) { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(standardPersonalityPanel); + + customPersonalityOpen = true; + }); + longitudinalToggle = standardPersonalityToggle; + } else if (param == "RelaxedPersonalityProfile") { + FrogPilotButtonsControl *relaxedPersonalityToggle = new FrogPilotButtonsControl(title, desc, icon, {tr("MANAGE")}); + QObject::connect(relaxedPersonalityToggle, &FrogPilotButtonsControl::buttonClicked, [longitudinalLayout, relaxedPersonalityPanel, this](int id) { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(relaxedPersonalityPanel); + + customPersonalityOpen = true; + }); + longitudinalToggle = relaxedPersonalityToggle; + } else if (aggressivePersonalityKeys.contains(param) || standardPersonalityKeys.contains(param) || relaxedPersonalityKeys.contains(param) || trafficPersonalityKeys.contains(param)) { + if (param == "TrafficFollow" || param == "AggressiveFollow" || param == "StandardFollow" || param == "RelaxedFollow") { + std::map followTimeLabels; + for (float i = 0; i <= 3; i += 0.01) { + followTimeLabels[i] = std::lround(i / 0.01) == 1 / 0.01 ? QString::number(i, 'f', 2) + tr(" second") : QString::number(i, 'f', 2) + tr(" seconds"); + } + if (param == "TrafficFollow") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0.5, 3, QString(), followTimeLabels, 0.01, true); + } else { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 3, QString(), followTimeLabels, 0.01, true); + } + } else { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 25, 200, "%"); + } + + } else if (param == "LongitudinalTune") { + FrogPilotManageControl *longitudinalTuneToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(longitudinalTuneToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, longitudinalTunePanel]() { + longitudinalLayout->setCurrentWidget(longitudinalTunePanel); + }); + longitudinalToggle = longitudinalTuneToggle; + } else if (param == "AccelerationProfile") { + std::vector accelerationProfiles{tr("Standard"), tr("Eco"), tr("Sport"), tr("Sport+")}; + ButtonParamControl *accelerationProfileToggle = new ButtonParamControl(param, title, desc, icon, accelerationProfiles); + longitudinalToggle = accelerationProfileToggle; + } else if (param == "DecelerationProfile") { + std::vector decelerationProfiles{tr("Standard"), tr("Eco"), tr("Sport")}; + ButtonParamControl *decelerationProfileToggle = new ButtonParamControl(param, title, desc, icon, decelerationProfiles); + longitudinalToggle = decelerationProfileToggle; + } else if (param == "LeadDetectionThreshold") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 25, 50, "%"); + + } else if (param == "QOLLongitudinal") { + FrogPilotManageControl *qolLongitudinalToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(qolLongitudinalToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, qolPanel]() { + longitudinalLayout->setCurrentWidget(qolPanel); + }); + longitudinalToggle = qolLongitudinalToggle; + } else if (param == "CustomCruise") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 99, tr(" mph")); + } else if (param == "CustomCruiseLong") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 1, 99, tr(" mph")); + } else if (param == "IncreasedStoppedDistance") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 10, tr(" feet")); + } else if (param == "MapGears") { + std::vector mapGearsToggles{"MapAcceleration", "MapDeceleration"}; + std::vector mapGearsToggleNames{tr("Acceleration"), tr("Deceleration")}; + longitudinalToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, mapGearsToggles, mapGearsToggleNames); + } else if (param == "SetSpeedOffset") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, tr(" mph")); + } else if (param == "WeatherPresets") { + FrogPilotManageControl *weatherToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(weatherToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, weatherPanel, this]() { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(weatherPanel); + + qolOpen = true; + }); + longitudinalToggle = weatherToggle; + } else if (param == "SetWeatherKey") { + weatherKeyControl = new FrogPilotButtonsControl(title, desc, icon, {tr("ADD"), tr("TEST")}); + QObject::connect(weatherKeyControl, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + if (id == 0) { + if (!params.get("WeatherToken").empty()) { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to remove your key?"), this)) { + params.remove("WeatherToken"); + + weatherKeyControl->setText(0, tr("ADD")); + weatherKeyControl->setVisibleButton(1, false); + } + } else { + int keyLength = 32; + QString currentKey = QString::fromStdString(params.get("WeatherToken")); + QString newKey = InputDialog::getText(tr("Enter your \"OpenWeatherMap\" key"), this, tr("Characters: 0/%1").arg(keyLength), false, -1, currentKey, keyLength).trimmed(); + if (!newKey.isEmpty()) { + params.put("WeatherToken", newKey.toStdString()); + + weatherKeyControl->setText(0, tr("REMOVE")); + weatherKeyControl->setVisibleButton(1, true); + } + } + } else if (id == 1) { + weatherKeyControl->setValue(tr("Testing...")); + + QString key = QString::fromStdString(params.get("WeatherToken")).trimmed(); + QString url30 = QString("https://api.openweathermap.org/data/3.0/onecall?lat=42.4293&lon=-83.9850&exclude=current,minutely,hourly,daily,alerts&appid=%1").arg(key); + + QNetworkRequest request(url30); + QNetworkReply *reply = networkManager->get(request); + QObject::connect(reply, &QNetworkReply::finished, this, [=]() { + reply->deleteLater(); + + if (reply->error() == QNetworkReply::NoError) { + weatherKeyControl->setValue(""); + ConfirmationDialog::alert(tr("Key is valid!"), this); + return; + } + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status == 401 || status == 403) { + QString url25 = QString("https://api.openweathermap.org/data/2.5/weather?lat=42.4293&lon=-83.9850&appid=%1").arg(key); + + QNetworkRequest request25(url25); + QNetworkReply *reply25 = networkManager->get(request25); + QObject::connect(reply25, &QNetworkReply::finished, this, [=]() { + reply25->deleteLater(); + + weatherKeyControl->setValue(""); + if (reply25->error() == QNetworkReply::NoError) { + ConfirmationDialog::alert(tr("Your key is valid for version 2.5, but version 3.0 is highly recommended! Please subscribe to the \"One Call API 3.0\" plan!"), this); + } else { + int status25 = reply25->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + ConfirmationDialog::alert(tr("Invalid key! (Error: %1)").arg(status25), this); + } + }); + } else { + weatherKeyControl->setValue(""); + ConfirmationDialog::alert(tr("An error occurred: %1").arg(reply->errorString()), this); + } + }); + } + }); + longitudinalToggle = weatherKeyControl; + } else if (param == "LowVisibilityOffsets") { + ButtonControl *manageLowVisibilitOffsetsButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(manageLowVisibilitOffsetsButton, &ButtonControl::clicked, [longitudinalLayout, weatherLowVisibilityPanel, this]() { + openSubSubSubPanel(); + + longitudinalLayout->setCurrentWidget(weatherLowVisibilityPanel); + + weatherOpen = true; + }); + longitudinalToggle = manageLowVisibilitOffsetsButton; + } else if (param == "RainOffsets") { + ButtonControl *manageRainOffsetsButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(manageRainOffsetsButton, &ButtonControl::clicked, [longitudinalLayout, weatherRainPanel, this]() { + openSubSubSubPanel(); + + longitudinalLayout->setCurrentWidget(weatherRainPanel); + + weatherOpen = true; + }); + longitudinalToggle = manageRainOffsetsButton; + } else if (param == "RainStormOffsets") { + ButtonControl *manageRainStormOffsetsButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(manageRainStormOffsetsButton, &ButtonControl::clicked, [longitudinalLayout, weatherRainStormPanel, this]() { + openSubSubSubPanel(); + + longitudinalLayout->setCurrentWidget(weatherRainStormPanel); + + weatherOpen = true; + }); + longitudinalToggle = manageRainStormOffsetsButton; + } else if (param == "SnowOffsets") { + ButtonControl *manageSnowOffsetsButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(manageSnowOffsetsButton, &ButtonControl::clicked, [longitudinalLayout, weatherSnowPanel, this]() { + openSubSubSubPanel(); + + longitudinalLayout->setCurrentWidget(weatherSnowPanel); + + weatherOpen = true; + }); + longitudinalToggle = manageSnowOffsetsButton; + } else if (param == "IncreaseFollowingLowVisibility" || param == "IncreaseFollowingRain" || param == "IncreaseFollowingRainStorm" || param == "IncreaseFollowingSnow") { + std::map followTimeLabels; + for (float i = 0; i <= 3; i += 0.01) { + followTimeLabels[i] = std::lround(i / 0.01) == 1 / 0.01 ? QString::number(i, 'f', 2) + tr(" second") : QString::number(i, 'f', 2) + tr(" seconds"); + } + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 3, QString(), followTimeLabels, 0.01, true); + } else if (param == "IncreasedStoppedDistanceLowVisibility" || param == "IncreasedStoppedDistanceRain" || param == "IncreasedStoppedDistanceRainStorm" || param == "IncreasedStoppedDistanceSnow") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 10, tr(" feet")); + } else if (param == "ReduceAccelerationLowVisibility" || param == "ReduceAccelerationRain" || param == "ReduceAccelerationRainStorm" || param == "ReduceAccelerationSnow") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, "%", std::map(), 1); + } else if (param == "ReduceLateralAccelerationLowVisibility" || param == "ReduceLateralAccelerationRain" || param == "ReduceLateralAccelerationRainStorm" || param == "ReduceLateralAccelerationSnow") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 99, "%", std::map(), 1); + + } else if (param == "SpeedLimitController") { + FrogPilotManageControl *speedLimitControllerToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(speedLimitControllerToggle, &FrogPilotManageControl::manageButtonClicked, [longitudinalLayout, speedLimitControllerPanel]() { + longitudinalLayout->setCurrentWidget(speedLimitControllerPanel); + }); + longitudinalToggle = speedLimitControllerToggle; + } else if (param == "SLCFallback") { + std::vector fallbackOptions{tr("Set Speed"), tr("Experimental Mode"), tr("Previous Limit")}; + ButtonParamControl *fallbackSelection = new ButtonParamControl(param, title, desc, icon, fallbackOptions); + longitudinalToggle = fallbackSelection; + } else if (param == "SLCOverride") { + std::vector overrideOptions{tr("None"), tr("Set With Gas Pedal"), tr("Max Set Speed")}; + ButtonParamControl *overrideSelection = new ButtonParamControl(param, title, desc, icon, overrideOptions); + longitudinalToggle = overrideSelection; + } else if (param == "SLCPriority") { + ButtonControl *slcPriorityButton = new ButtonControl(title, tr("SELECT"), desc); + QStringList primaryPriorities = {tr("Dashboard"), tr("Map Data"), tr("Highest"), tr("Lowest")}; + QStringList otherPriorities = {tr("None"), tr("Dashboard"), tr("Map Data")}; + QStringList priorityPrompts = {tr("Select your primary priority"), tr("Select your secondary priority")}; + + QObject::connect(slcPriorityButton, &ButtonControl::clicked, [=]() { + QStringList selectedPriorities; + + for (int i = 1; i <= 2; ++i) { + QStringList availablePriorities = i == 1 ? primaryPriorities : otherPriorities; + availablePriorities = availablePriorities.toSet().subtract(selectedPriorities.toSet()).toList(); + + if (!parent->hasDashSpeedLimits) { + availablePriorities.removeAll(tr("Dashboard")); + } + if (availablePriorities.size() == 1 && availablePriorities.contains(tr("None"))) { + break; + } + + QString selection = MultiOptionDialog::getSelection(priorityPrompts[i - 1], availablePriorities, "", this); + if (selection.isEmpty()) { + break; + } + + selectedPriorities.append(selection); + + params.put(QString("SLCPriority%1").arg(i).toStdString(), selection.toStdString()); + if (selection == tr("None")) { + for (int j = i + 1; j <= 3; ++j) { + params.put(QString("SLCPriority%1").arg(j).toStdString(), tr("None").toStdString()); + } + break; + } + + if (selection == tr("Lowest") || selection == tr("Highest")) { + break; + } + } + + selectedPriorities.removeAll(tr("None")); + if (!selectedPriorities.isEmpty()) { + slcPriorityButton->setValue(selectedPriorities.join(", ")); + } + }); + + QStringList selectedPriorities; + for (int i = 1; i <= 2; ++i) { + QString priority = QString::fromStdString(params.get(QString("SLCPriority%1").arg(i).toStdString())); + if (primaryPriorities.contains(priority)) { + selectedPriorities.append(priority); + } + } + slcPriorityButton->setValue(selectedPriorities.join(", ")); + + longitudinalToggle = slcPriorityButton; + } else if (param == "SLCOffsets") { + ButtonControl *manageSLCOffsetsButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(manageSLCOffsetsButton, &ButtonControl::clicked, [longitudinalLayout, speedLimitControllerOffsetsPanel, this]() { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(speedLimitControllerOffsetsPanel); + + slcOpen = true; + }); + longitudinalToggle = manageSLCOffsetsButton; + } else if (speedLimitControllerOffsetsKeys.contains(param)) { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, -99, 99, tr(" mph")); + } else if (param == "SLCQOL") { + ButtonControl *manageSLCQOLButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(manageSLCQOLButton, &ButtonControl::clicked, [longitudinalLayout, speedLimitControllerQOLPanel, this]() { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(speedLimitControllerQOLPanel); + + slcOpen = true; + }); + longitudinalToggle = manageSLCQOLButton; + } else if (param == "SLCConfirmation") { + std::vector confirmationToggles{"SLCConfirmationLower", "SLCConfirmationHigher"}; + std::vector confirmationToggleNames{tr("Lower Limits"), tr("Higher Limits")}; + longitudinalToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, confirmationToggles, confirmationToggleNames); + } else if (param == "SLCLookaheadHigher" || param == "SLCLookaheadLower") { + longitudinalToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 30, tr(" seconds")); + } else if (param == "SLCVisuals") { + ButtonControl *manageSLCVisualsButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(manageSLCVisualsButton, &ButtonControl::clicked, [longitudinalLayout, speedLimitControllerVisualPanel, this]() { + openSubSubPanel(); + + longitudinalLayout->setCurrentWidget(speedLimitControllerVisualPanel); + + slcOpen = true; + }); + longitudinalToggle = manageSLCVisualsButton; + + } else { + longitudinalToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = longitudinalToggle; + + if (advancedLongitudinalTuneKeys.contains(param)) { + advancedLongitudinalTuneList->addItem(longitudinalToggle); + } else if (aggressivePersonalityKeys.contains(param)) { + aggressivePersonalityList->addItem(longitudinalToggle); + } else if (conditionalExperimentalKeys.contains(param)) { + conditionalExperimentalList->addItem(longitudinalToggle); + } else if (curveSpeedKeys.contains(param)) { + curveSpeedList->addItem(longitudinalToggle); + } else if (customDrivingPersonalityKeys.contains(param)) { + customDrivingPersonalityList->addItem(longitudinalToggle); + } else if (longitudinalTuneKeys.contains(param)) { + longitudinalTuneList->addItem(longitudinalToggle); + } else if (qolKeys.contains(param)) { + qolList->addItem(longitudinalToggle); + } else if (relaxedPersonalityKeys.contains(param)) { + relaxedPersonalityList->addItem(longitudinalToggle); + } else if (speedLimitControllerKeys.contains(param)) { + speedLimitControllerList->addItem(longitudinalToggle); + } else if (speedLimitControllerOffsetsKeys.contains(param)) { + speedLimitControllerOffsetsList->addItem(longitudinalToggle); + } else if (speedLimitControllerQOLKeys.contains(param)) { + speedLimitControllerQOLList->addItem(longitudinalToggle); + } else if (speedLimitControllerVisualKeys.contains(param)) { + speedLimitControllerVisualList->addItem(longitudinalToggle); + } else if (standardPersonalityKeys.contains(param)) { + standardPersonalityList->addItem(longitudinalToggle); + } else if (trafficPersonalityKeys.contains(param)) { + trafficPersonalityList->addItem(longitudinalToggle); + } else if (weatherKeys.contains(param)) { + weatherList->addItem(longitudinalToggle); + } else if (weatherLowVisibilityKeys.contains(param)) { + weatherLowVisibilityList->addItem(longitudinalToggle); + } else if (weatherRainKeys.contains(param)) { + weatherRainList->addItem(longitudinalToggle); + } else if (weatherRainStormKeys.contains(param)) { + weatherRainStormList->addItem(longitudinalToggle); + } else if (weatherSnowKeys.contains(param)) { + weatherSnowList->addItem(longitudinalToggle); + } else { + longitudinalList->addItem(longitudinalToggle); + + parentKeys.insert(param); + } + + if (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(longitudinalToggle)) { + QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + emit openSubPanel(); + openDescriptions(forceOpenDescriptions, toggles); + }); + } + + QObject::connect(longitudinalToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(longitudinalToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + QSet forceUpdateKeys = {"HumanAcceleration", "LongitudinalTune"}; + for (const QString &key : forceUpdateKeys) { + QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, this, &FrogPilotLongitudinalPanel::updateToggles); + } + + FrogPilotParamValueControl *trafficFollowToggle = static_cast(toggles["TrafficFollow"]); + FrogPilotParamValueControl *trafficAccelerationToggle = static_cast(toggles["TrafficJerkAcceleration"]); + FrogPilotParamValueControl *trafficDecelerationToggle = static_cast(toggles["TrafficJerkDeceleration"]); + FrogPilotParamValueControl *trafficDangerToggle = static_cast(toggles["TrafficJerkDanger"]); + FrogPilotParamValueControl *trafficSpeedToggle = static_cast(toggles["TrafficJerkSpeed"]); + FrogPilotParamValueControl *trafficSpeedDecreaseToggle = static_cast(toggles["TrafficJerkSpeedDecrease"]); + FrogPilotButtonsControl *trafficResetButton = static_cast(toggles["ResetTrafficPersonality"]); + QObject::connect(trafficResetButton, &FrogPilotButtonsControl::buttonClicked, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for Traffic Mode?"), this)) { + params.putFloat("TrafficFollow", std::stof(params.getKeyDefaultValue("TrafficFollow").value())); + params.putFloat("TrafficJerkAcceleration", std::stof(params.getKeyDefaultValue("TrafficJerkAcceleration").value())); + params.putFloat("TrafficJerkDeceleration", std::stof(params.getKeyDefaultValue("TrafficJerkDeceleration").value())); + params.putFloat("TrafficJerkDanger", std::stof(params.getKeyDefaultValue("TrafficJerkDanger").value())); + params.putFloat("TrafficJerkSpeed", std::stof(params.getKeyDefaultValue("TrafficJerkSpeed").value())); + params.putFloat("TrafficJerkSpeedDecrease", std::stof(params.getKeyDefaultValue("TrafficJerkSpeedDecrease").value())); + + trafficFollowToggle->refresh(); + trafficAccelerationToggle->refresh(); + trafficDecelerationToggle->refresh(); + trafficDangerToggle->refresh(); + trafficSpeedToggle->refresh(); + trafficSpeedDecreaseToggle->refresh(); + } + }); + + FrogPilotParamValueControl *aggressiveFollowToggle = static_cast(toggles["AggressiveFollow"]); + FrogPilotParamValueControl *aggressiveAccelerationToggle = static_cast(toggles["AggressiveJerkAcceleration"]); + FrogPilotParamValueControl *aggressiveDecelerationToggle = static_cast(toggles["AggressiveJerkDeceleration"]); + FrogPilotParamValueControl *aggressiveDangerToggle = static_cast(toggles["AggressiveJerkDanger"]); + FrogPilotParamValueControl *aggressiveSpeedToggle = static_cast(toggles["AggressiveJerkSpeed"]); + FrogPilotParamValueControl *aggressiveSpeedDecreaseToggle = static_cast(toggles["AggressiveJerkSpeedDecrease"]); + FrogPilotButtonsControl *aggressiveResetButton = static_cast(toggles["ResetAggressivePersonality"]); + QObject::connect(aggressiveResetButton, &FrogPilotButtonsControl::buttonClicked, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for the Aggressive personality?"), this)) { + params.putFloat("AggressiveFollow", std::stof(params.getKeyDefaultValue("AggressiveFollow").value())); + params.putFloat("AggressiveJerkAcceleration", std::stof(params.getKeyDefaultValue("AggressiveJerkAcceleration").value())); + params.putFloat("AggressiveJerkDeceleration", std::stof(params.getKeyDefaultValue("AggressiveJerkDeceleration").value())); + params.putFloat("AggressiveJerkDanger", std::stof(params.getKeyDefaultValue("AggressiveJerkDanger").value())); + params.putFloat("AggressiveJerkSpeed", std::stof(params.getKeyDefaultValue("AggressiveJerkSpeed").value())); + params.putFloat("AggressiveJerkSpeedDecrease", std::stof(params.getKeyDefaultValue("AggressiveJerkSpeedDecrease").value())); + + aggressiveFollowToggle->refresh(); + aggressiveAccelerationToggle->refresh(); + aggressiveDecelerationToggle->refresh(); + aggressiveDangerToggle->refresh(); + aggressiveSpeedToggle->refresh(); + aggressiveSpeedDecreaseToggle->refresh(); + } + }); + + FrogPilotParamValueControl *standardFollowToggle = static_cast(toggles["StandardFollow"]); + FrogPilotParamValueControl *standardAccelerationToggle = static_cast(toggles["StandardJerkAcceleration"]); + FrogPilotParamValueControl *standardDecelerationToggle = static_cast(toggles["StandardJerkDeceleration"]); + FrogPilotParamValueControl *standardDangerToggle = static_cast(toggles["StandardJerkDanger"]); + FrogPilotParamValueControl *standardSpeedToggle = static_cast(toggles["StandardJerkSpeed"]); + FrogPilotParamValueControl *standardSpeedDecreaseToggle = static_cast(toggles["StandardJerkSpeedDecrease"]); + FrogPilotButtonsControl *standardResetButton = static_cast(toggles["ResetStandardPersonality"]); + QObject::connect(standardResetButton, &FrogPilotButtonsControl::buttonClicked, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for the Standard personality?"), this)) { + params.putFloat("StandardFollow", std::stof(params.getKeyDefaultValue("StandardFollow").value())); + params.putFloat("StandardJerkAcceleration", std::stof(params.getKeyDefaultValue("StandardJerkAcceleration").value())); + params.putFloat("StandardJerkDeceleration", std::stof(params.getKeyDefaultValue("StandardJerkDeceleration").value())); + params.putFloat("StandardJerkDanger", std::stof(params.getKeyDefaultValue("StandardJerkDanger").value())); + params.putFloat("StandardJerkSpeed", std::stof(params.getKeyDefaultValue("StandardJerkSpeed").value())); + params.putFloat("StandardJerkSpeedDecrease", std::stof(params.getKeyDefaultValue("StandardJerkSpeedDecrease").value())); + + standardFollowToggle->refresh(); + standardAccelerationToggle->refresh(); + standardDecelerationToggle->refresh(); + standardDangerToggle->refresh(); + standardSpeedToggle->refresh(); + standardSpeedDecreaseToggle->refresh(); + } + }); + + FrogPilotParamValueControl *relaxedFollowToggle = static_cast(toggles["RelaxedFollow"]); + FrogPilotParamValueControl *relaxedAccelerationToggle = static_cast(toggles["RelaxedJerkAcceleration"]); + FrogPilotParamValueControl *relaxedDecelerationToggle = static_cast(toggles["RelaxedJerkDeceleration"]); + FrogPilotParamValueControl *relaxedDangerToggle = static_cast(toggles["RelaxedJerkDanger"]); + FrogPilotParamValueControl *relaxedSpeedToggle = static_cast(toggles["RelaxedJerkSpeed"]); + FrogPilotParamValueControl *relaxedSpeedDecreaseToggle = static_cast(toggles["RelaxedJerkSpeedDecrease"]); + FrogPilotButtonsControl *relaxedResetButton = static_cast(toggles["ResetRelaxedPersonality"]); + QObject::connect(relaxedResetButton, &FrogPilotButtonsControl::buttonClicked, [=]() { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your settings for the Relaxed personality?"), this)) { + params.putFloat("RelaxedFollow", std::stof(params.getKeyDefaultValue("RelaxedFollow").value())); + params.putFloat("RelaxedJerkAcceleration", std::stof(params.getKeyDefaultValue("RelaxedJerkAcceleration").value())); + params.putFloat("RelaxedJerkDeceleration", std::stof(params.getKeyDefaultValue("RelaxedJerkDeceleration").value())); + params.putFloat("RelaxedJerkDanger", std::stof(params.getKeyDefaultValue("RelaxedJerkDanger").value())); + params.putFloat("RelaxedJerkSpeed", std::stof(params.getKeyDefaultValue("RelaxedJerkSpeed").value())); + params.putFloat("RelaxedJerkSpeedDecrease", std::stof(params.getKeyDefaultValue("RelaxedJerkSpeedDecrease").value())); + + relaxedFollowToggle->refresh(); + relaxedAccelerationToggle->refresh(); + relaxedDecelerationToggle->refresh(); + relaxedDangerToggle->refresh(); + relaxedSpeedToggle->refresh(); + relaxedSpeedDecreaseToggle->refresh(); + } + }); + + openDescriptions(forceOpenDescriptions, toggles); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [longitudinalLayout, longitudinalPanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + longitudinalLayout->setCurrentWidget(longitudinalPanel); + }); + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubSubPanel, [longitudinalLayout, customDrivingPersonalityPanel, qolPanel, speedLimitControllerPanel, this]() { + openDescriptions(forceOpenDescriptions, toggles); + + if (customPersonalityOpen) { + longitudinalLayout->setCurrentWidget(customDrivingPersonalityPanel); + + customPersonalityOpen = false; + } else if (qolOpen) { + longitudinalLayout->setCurrentWidget(qolPanel); + + qolOpen = false; + } else if (slcOpen) { + longitudinalLayout->setCurrentWidget(speedLimitControllerPanel); + + slcOpen = false; + } + }); + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubSubSubPanel, [longitudinalLayout, weatherPanel, this]() { + openDescriptions(forceOpenDescriptions, toggles); + + if (weatherOpen) { + longitudinalLayout->setCurrentWidget(weatherPanel); + + weatherOpen = false; + } + }); + QObject::connect(parent, &FrogPilotSettingsWindow::updateMetric, this, &FrogPilotLongitudinalPanel::updateMetric); +} + +void FrogPilotLongitudinalPanel::showEvent(QShowEvent *event) { + FrogPilotUIState &fs = *frogpilotUIState(); + + calibratedLateralAccelerationLabel->setText(QString::number(params.getFloat("CalibratedLateralAcceleration"), 'f', 2) + tr(" m/s²")); + calibrationProgressLabel->setText(QString::number(params.getFloat("CalibrationProgress"), 'f', 2) + "%"); + + longitudinalActuatorDelayToggle->setTitle(QString(tr("Actuator Delay (Default: %1)")).arg(QString::number(parent->longitudinalActuatorDelay, 'f', 2))); + startAccelToggle->setTitle(QString(tr("Start Acceleration (Default: %1)")).arg(QString::number(parent->startAccel, 'f', 2))); + stopAccelToggle->setTitle(QString(tr("Stop Acceleration (Default: %1)")).arg(QString::number(parent->stopAccel, 'f', 2))); + stoppingDecelRateToggle->setTitle(QString(tr("Stopping Rate (Default: %1)")).arg(QString::number(parent->stoppingDecelRate, 'f', 2))); + vEgoStartingToggle->setTitle(QString(tr("Start Speed (Default: %1)")).arg(QString::number(parent->vEgoStarting, 'f', 2))); + vEgoStoppingToggle->setTitle(QString(tr("Stop Speed (Default: %1)")).arg(QString::number(parent->vEgoStopping, 'f', 2))); + + bool keyExists = !params.get("WeatherToken").empty(); + weatherKeyControl->setText(0, keyExists ? tr("REMOVE") : tr("ADD")); + weatherKeyControl->setVisibleButton(1, keyExists && fs.frogpilot_scene.online); + + updateToggles(); +} + +void FrogPilotLongitudinalPanel::updateMetric(bool metric, bool bootRun) { + static bool previousMetric; + if (metric != previousMetric && !bootRun) { + double distanceConversion = metric ? FOOT_TO_METER : METER_TO_FOOT; + double speedConversion = metric ? MILE_TO_KM : KM_TO_MILE; + + params.putIntNonBlocking("IncreasedStoppedDistance", params.getInt("IncreasedStoppedDistance") * distanceConversion); + params.putIntNonBlocking("IncreasedStoppedDistanceLowVisibility", params.getInt("IncreasedStoppedDistanceLowVisibility") * distanceConversion); + params.putIntNonBlocking("IncreasedStoppedDistanceRain", params.getInt("IncreasedStoppedDistanceRain") * distanceConversion); + params.putIntNonBlocking("IncreasedStoppedDistanceRainStorm", params.getInt("IncreasedStoppedDistanceRainStorm") * distanceConversion); + params.putIntNonBlocking("IncreasedStoppedDistanceSnow", params.getInt("IncreasedStoppedDistanceSnow") * distanceConversion); + + params.putIntNonBlocking("CESignalSpeed", params.getInt("CESignalSpeed") * speedConversion); + params.putIntNonBlocking("CESpeed", params.getInt("CESpeed") * speedConversion); + params.putIntNonBlocking("CESpeedLead", params.getInt("CESpeedLead") * speedConversion); + params.putIntNonBlocking("CustomCruise", params.getInt("CustomCruise") * speedConversion); + params.putIntNonBlocking("CustomCruiseLong", params.getInt("CustomCruiseLong") * speedConversion); + params.putIntNonBlocking("Offset1", params.getInt("Offset1") * speedConversion); + params.putIntNonBlocking("Offset2", params.getInt("Offset2") * speedConversion); + params.putIntNonBlocking("Offset3", params.getInt("Offset3") * speedConversion); + params.putIntNonBlocking("Offset4", params.getInt("Offset4") * speedConversion); + params.putIntNonBlocking("Offset5", params.getInt("Offset5") * speedConversion); + params.putIntNonBlocking("Offset6", params.getInt("Offset6") * speedConversion); + params.putIntNonBlocking("Offset7", params.getInt("Offset7") * speedConversion); + params.putIntNonBlocking("SetSpeedOffset", params.getInt("SetSpeedOffset") * speedConversion); + } + previousMetric = metric; + + static std::map imperialDistanceLabels; + static std::map imperialSpeedLabels; + static std::map metricDistanceLabels; + static std::map metricSpeedLabels; + + static bool labelsInitialized = false; + if (!labelsInitialized) { + for (int i = 0; i <= 10; ++i) { + imperialDistanceLabels[i] = i == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" foot") : QString::number(i) + tr(" feet"); + } + + for (int i = 0; i <= 99; ++i) { + imperialSpeedLabels[i] = i == 0 ? tr("Off") : QString::number(i) + tr(" mph"); + } + + for (int i = 0; i <= 3; ++i) { + metricDistanceLabels[i] = i == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" meter") : QString::number(i) + tr(" meters"); + } + + for (int i = 0; i <= 150; ++i) { + metricSpeedLabels[i] = i == 0 ? tr("Off") : QString::number(i) + tr(" km/h"); + } + + labelsInitialized = true; + } + + FrogPilotDualParamValueControl *ceSpeedToggle = reinterpret_cast(toggles["CESpeed"]); + FrogPilotParamValueButtonControl *ceSignal = static_cast(toggles["CESignalSpeed"]); + FrogPilotParamValueControl *customCruiseToggle = static_cast(toggles["CustomCruise"]); + FrogPilotParamValueControl *customCruiseLongToggle = static_cast(toggles["CustomCruiseLong"]); + 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 *offset5Toggle = static_cast(toggles["Offset5"]); + FrogPilotParamValueControl *offset6Toggle = static_cast(toggles["Offset6"]); + FrogPilotParamValueControl *offset7Toggle = static_cast(toggles["Offset7"]); + FrogPilotParamValueControl *increasedStoppedDistanceToggle = static_cast(toggles["IncreasedStoppedDistance"]); + FrogPilotParamValueControl *increasedStoppedDistanceLowVisibilityToggle = static_cast(toggles["IncreasedStoppedDistanceLowVisibility"]); + FrogPilotParamValueControl *increasedStoppedDistanceRainToggle = static_cast(toggles["IncreasedStoppedDistanceRain"]); + FrogPilotParamValueControl *increasedStoppedDistanceRainStormToggle = static_cast(toggles["IncreasedStoppedDistanceRainStorm"]); + FrogPilotParamValueControl *increasedStoppedDistanceSnowToggle = static_cast(toggles["IncreasedStoppedDistanceSnow"]); + FrogPilotParamValueControl *setSpeedOffsetToggle = static_cast(toggles["SetSpeedOffset"]); + + if (metric) { + offset1Toggle->setTitle(tr("Speed Offset (0–29 km/h)")); + offset2Toggle->setTitle(tr("Speed Offset (30–49 km/h)")); + offset3Toggle->setTitle(tr("Speed Offset (50–59 km/h)")); + offset4Toggle->setTitle(tr("Speed Offset (60–79 km/h)")); + offset5Toggle->setTitle(tr("Speed Offset (80–99 km/h)")); + offset6Toggle->setTitle(tr("Speed Offset (100–119 km/h)")); + offset7Toggle->setTitle(tr("Speed Offset (120–140 km/h)")); + + offset1Toggle->setDescription(tr("How much to offset posted speed-limits between 0 and 24 mph.")); + offset2Toggle->setDescription(tr("How much to offset posted speed-limits between 25 and 34 mph.")); + offset3Toggle->setDescription(tr("How much to offset posted speed-limits between 35 and 44 mph.")); + offset4Toggle->setDescription(tr("How much to offset posted speed-limits between 45 and 54 mph.")); + offset5Toggle->setDescription(tr("How much to offset posted speed-limits between 55 and 64 mph.")); + offset6Toggle->setDescription(tr("How much to offset posted speed-limits between 65 and 74 mph.")); + offset7Toggle->setDescription(tr("How much to offset posted speed-limits between 75 and 99 mph.")); + + increasedStoppedDistanceToggle->updateControl(0, 3, metricDistanceLabels); + increasedStoppedDistanceLowVisibilityToggle->updateControl(0, 3, metricDistanceLabels); + increasedStoppedDistanceRainToggle->updateControl(0, 3, metricDistanceLabels); + increasedStoppedDistanceRainStormToggle->updateControl(0, 3, metricDistanceLabels); + increasedStoppedDistanceSnowToggle->updateControl(0, 3, metricDistanceLabels); + + ceSignal->updateControl(0, 150, metricSpeedLabels); + ceSpeedToggle->updateControl(0, 150, metricSpeedLabels); + customCruiseToggle->updateControl(1, 150, metricSpeedLabels); + customCruiseLongToggle->updateControl(1, 150, metricSpeedLabels); + offset1Toggle->updateControl(-150, 150, metricSpeedLabels); + offset2Toggle->updateControl(-150, 150, metricSpeedLabels); + offset3Toggle->updateControl(-150, 150, metricSpeedLabels); + offset4Toggle->updateControl(-150, 150, metricSpeedLabels); + offset5Toggle->updateControl(-150, 150, metricSpeedLabels); + offset6Toggle->updateControl(-150, 150, metricSpeedLabels); + offset7Toggle->updateControl(-150, 150, metricSpeedLabels); + setSpeedOffsetToggle->updateControl(-150, 150, metricSpeedLabels); + } else { + offset1Toggle->setTitle(tr("Speed Offset (0–24 mph)")); + offset2Toggle->setTitle(tr("Speed Offset (25–34 mph)")); + offset3Toggle->setTitle(tr("Speed Offset (35–44 mph)")); + offset4Toggle->setTitle(tr("Speed Offset (45–54 mph)")); + offset5Toggle->setTitle(tr("Speed Offset (55–64 mph)")); + offset6Toggle->setTitle(tr("Speed Offset (65–74 mph)")); + offset7Toggle->setTitle(tr("Speed Offset (75–99 mph)")); + + offset1Toggle->setDescription(tr("How much to offset posted speed-limits between 0 and 24 mph.")); + offset2Toggle->setDescription(tr("How much to offset posted speed-limits between 25 and 34 mph.")); + offset3Toggle->setDescription(tr("How much to offset posted speed-limits between 35 and 44 mph.")); + offset4Toggle->setDescription(tr("How much to offset posted speed-limits between 45 and 54 mph.")); + offset5Toggle->setDescription(tr("How much to offset posted speed-limits between 55 and 64 mph.")); + offset6Toggle->setDescription(tr("How much to offset posted speed-limits between 65 and 74 mph.")); + offset7Toggle->setDescription(tr("How much to offset posted speed-limits between 75 and 99 mph.")); + + increasedStoppedDistanceToggle->updateControl(0, 10, imperialDistanceLabels); + increasedStoppedDistanceLowVisibilityToggle->updateControl(0, 10, imperialDistanceLabels); + increasedStoppedDistanceRainToggle->updateControl(0, 10, imperialDistanceLabels); + increasedStoppedDistanceRainStormToggle->updateControl(0, 10, imperialDistanceLabels); + increasedStoppedDistanceSnowToggle->updateControl(0, 10, imperialDistanceLabels); + + ceSignal->updateControl(0, 99, imperialSpeedLabels); + ceSpeedToggle->updateControl(0, 99, imperialSpeedLabels); + customCruiseToggle->updateControl(1, 99, imperialSpeedLabels); + customCruiseLongToggle->updateControl(1, 99, imperialSpeedLabels); + offset1Toggle->updateControl(-99, 99, imperialSpeedLabels); + offset2Toggle->updateControl(-99, 99, imperialSpeedLabels); + offset3Toggle->updateControl(-99, 99, imperialSpeedLabels); + offset4Toggle->updateControl(-99, 99, imperialSpeedLabels); + offset5Toggle->updateControl(-99, 99, imperialSpeedLabels); + offset6Toggle->updateControl(-99, 99, imperialSpeedLabels); + offset7Toggle->updateControl(-99, 99, imperialSpeedLabels); + setSpeedOffsetToggle->updateControl(0, 99, imperialSpeedLabels); + } +} + +void FrogPilotLongitudinalPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "CEStopLights") { + setVisible &= parent->tuningLevel < parent->frogpilotToggleLevels["CEModelStopTime"].toDouble(); + } + + else if (key == "CustomCruise" || key == "CustomCruiseLong" || key == "SetSpeedLimit" || key == "SetSpeedOffset") { + setVisible &= !parent->hasPCMCruise; + } + + else if (key == "HumanLaneChanges") { + setVisible &= parent->hasRadar; + } + + else if (key == "MapGears") { + setVisible &= parent->isToyota; + setVisible &= !parent->isTSK; + } + + else if (key == "ReverseCruise") { + setVisible &= parent->isToyota; + } + + else if (key == "SLCMapboxFiller") { + setVisible &= !params.get("MapboxSecretKey").empty(); + } + + else if (key == "StartAccel") { + setVisible &= !(params.getBool("LongitudinalTune") && params.getBool("HumanAcceleration")); + } + + else if (key == "StoppingDecelRate" || key == "VEgoStarting" || key == "VEgoStopping") { + setVisible &= !parent->isGM || !params.getBool("ExperimentalGMTune"); + setVisible &= !parent->isToyota || !params.getBool("FrogsGoMoosTweak"); + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (advancedLongitudinalTuneKeys.contains(key)) { + toggles["AdvancedLongitudinalTune"]->setVisible(true); + } else if (aggressivePersonalityKeys.contains(key)) { + toggles["AggressivePersonalityProfile"]->setVisible(true); + } else if (conditionalExperimentalKeys.contains(key)) { + toggles["ConditionalExperimental"]->setVisible(true); + } else if (curveSpeedKeys.contains(key)) { + toggles["CurveSpeedController"]->setVisible(true); + } else if (customDrivingPersonalityKeys.contains(key)) { + toggles["CustomPersonalities"]->setVisible(true); + } else if (longitudinalTuneKeys.contains(key)) { + toggles["LongitudinalTune"]->setVisible(true); + } else if (qolKeys.contains(key)) { + toggles["QOLLongitudinal"]->setVisible(true); + } else if (relaxedPersonalityKeys.contains(key)) { + toggles["RelaxedPersonalityProfile"]->setVisible(true); + } else if (speedLimitControllerKeys.contains(key)) { + toggles["SpeedLimitController"]->setVisible(true); + } else if (speedLimitControllerOffsetsKeys.contains(key)) { + toggles["SLCOffsets"]->setVisible(true); + } else if (speedLimitControllerQOLKeys.contains(key)) { + toggles["SLCQOL"]->setVisible(true); + } else if (speedLimitControllerVisualKeys.contains(key)) { + toggles["SLCVisuals"]->setVisible(true); + } else if (standardPersonalityKeys.contains(key)) { + toggles["StandardPersonalityProfile"]->setVisible(true); + } else if (trafficPersonalityKeys.contains(key)) { + toggles["TrafficPersonalityProfile"]->setVisible(true); + } + } + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/longitudinal_settings.h b/frogpilot/ui/qt/offroad/longitudinal_settings.h new file mode 100644 index 000000000..78bc9ceb5 --- /dev/null +++ b/frogpilot/ui/qt/offroad/longitudinal_settings.h @@ -0,0 +1,70 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotLongitudinalPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotLongitudinalPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + void openSubSubPanel(); + void openSubSubSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateMetric(bool metric, bool bootRun); + void updateToggles(); + + bool customPersonalityOpen; + bool forceOpenDescriptions; + bool qolOpen; + bool slcOpen; + bool weatherOpen; + + std::map toggles; + + QSet advancedLongitudinalTuneKeys = {"LongitudinalActuatorDelay", "MaxDesiredAcceleration", "StartAccel", "StopAccel", "StoppingDecelRate", "VEgoStarting", "VEgoStopping"}; + QSet aggressivePersonalityKeys = {"AggressiveFollow", "AggressiveJerkAcceleration", "AggressiveJerkDeceleration", "AggressiveJerkDanger", "AggressiveJerkSpeed", "AggressiveJerkSpeedDecrease", "ResetAggressivePersonality"}; + QSet conditionalExperimentalKeys = {"CESpeed", "CESpeedLead", "CECurves", "CELead", "CEModelStopTime", "CESignalSpeed", "CEStopLights", "ShowCEMStatus"}; + QSet curveSpeedKeys = {"CalibratedLateralAcceleration", "CalibrationProgress", "ResetCurveData", "ShowCSCStatus"}; + QSet customDrivingPersonalityKeys = {"AggressivePersonalityProfile", "RelaxedPersonalityProfile", "StandardPersonalityProfile", "TrafficPersonalityProfile"}; + QSet longitudinalTuneKeys = {"AccelerationProfile", "DecelerationProfile", "HumanAcceleration", "HumanFollowing", "HumanLaneChanges", "LeadDetectionThreshold", "TacoTune"}; + QSet qolKeys = {"CustomCruise", "CustomCruiseLong", "ForceStops", "IncreasedStoppedDistance", "MapGears", "ReverseCruise", "SetSpeedOffset", "WeatherPresets"}; + QSet relaxedPersonalityKeys = {"RelaxedFollow", "RelaxedJerkAcceleration", "RelaxedJerkDeceleration", "RelaxedJerkDanger", "RelaxedJerkSpeed", "RelaxedJerkSpeedDecrease", "ResetRelaxedPersonality"}; + QSet speedLimitControllerKeys = {"SLCOffsets", "SLCFallback", "SLCOverride", "SLCPriority", "SLCQOL", "SLCVisuals"}; + QSet speedLimitControllerOffsetsKeys = {"Offset1", "Offset2", "Offset3", "Offset4", "Offset5", "Offset6", "Offset7"}; + QSet speedLimitControllerQOLKeys = {"SetSpeedLimit", "SLCConfirmation", "SLCLookaheadHigher", "SLCLookaheadLower", "SLCMapboxFiller"}; + QSet speedLimitControllerVisualKeys = {"ShowSLCOffset", "SpeedLimitSources"}; + QSet standardPersonalityKeys = {"StandardFollow", "StandardJerkAcceleration", "StandardJerkDeceleration", "StandardJerkDanger", "StandardJerkSpeed", "StandardJerkSpeedDecrease", "ResetStandardPersonality"}; + QSet trafficPersonalityKeys = {"TrafficFollow", "TrafficJerkAcceleration", "TrafficJerkDeceleration", "TrafficJerkDanger", "TrafficJerkSpeed", "TrafficJerkSpeedDecrease", "ResetTrafficPersonality"}; + QSet weatherKeys = {"LowVisibilityOffsets", "RainOffsets", "RainStormOffsets", "SetWeatherKey", "SnowOffsets"}; + QSet weatherLowVisibilityKeys = {"IncreaseFollowingLowVisibility", "IncreasedStoppedDistanceLowVisibility", "ReduceAccelerationLowVisibility", "ReduceLateralAccelerationLowVisibility"}; + QSet weatherRainKeys = {"IncreaseFollowingRain", "IncreasedStoppedDistanceRain", "ReduceAccelerationRain", "ReduceLateralAccelerationRain"}; + QSet weatherRainStormKeys = {"IncreaseFollowingRainStorm", "IncreasedStoppedDistanceRainStorm", "ReduceAccelerationRainStorm", "ReduceLateralAccelerationRainStorm"}; + QSet weatherSnowKeys = {"IncreaseFollowingSnow", "IncreasedStoppedDistanceSnow", "ReduceAccelerationSnow", "ReduceLateralAccelerationSnow"}; + + QSet parentKeys; + + FrogPilotButtonsControl *weatherKeyControl; + + FrogPilotParamValueControl *longitudinalActuatorDelayToggle; + FrogPilotParamValueControl *startAccelToggle; + FrogPilotParamValueControl *stopAccelToggle; + FrogPilotParamValueControl *stoppingDecelRateToggle; + FrogPilotParamValueControl *vEgoStartingToggle; + FrogPilotParamValueControl *vEgoStoppingToggle; + + FrogPilotSettingsWindow *parent; + + LabelControl *calibratedLateralAccelerationLabel; + LabelControl *calibrationProgressLabel; + + Params params; + + QNetworkAccessManager *networkManager; +}; diff --git a/frogpilot/ui/qt/offroad/maps_settings.cc b/frogpilot/ui/qt/offroad/maps_settings.cc new file mode 100644 index 000000000..3e0913411 --- /dev/null +++ b/frogpilot/ui/qt/offroad/maps_settings.cc @@ -0,0 +1,312 @@ +#include + +#include + +#include "frogpilot/ui/qt/offroad/maps_settings.h" + +FrogPilotMapsPanel::FrogPilotMapsPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *mapsLayout = new QStackedLayout(); + addItem(mapsLayout); + + FrogPilotListWidget *settingsList = new FrogPilotListWidget(this); + + std::vector scheduleOptions{tr("Manually"), tr("Weekly"), tr("Monthly")}; + preferredSchedule = new ButtonParamControl("PreferredSchedule", tr("Automatically Update Maps"), + tr("How often maps update from \"OpenStreetMap (OSM)\" with the latest speed limit information. " + "Weekly updates run every Sunday; monthly updates run on the 1st."), + "", + scheduleOptions); + settingsList->addItem(preferredSchedule); + + downloadMapsButton = new ButtonControl(tr("Download Maps"), tr("DOWNLOAD"), tr("Manually update your selected map sources so \"Speed Limit Controller\" has the latest speed limit information.")); + QObject::connect(downloadMapsButton, &ButtonControl::clicked, [this] { + if (downloadMapsButton->text() == tr("CANCEL")) { + if (FrogPilotConfirmationDialog::yesorno(tr("Cancel the download?"), this)) { + cancelDownload(); + } + } else { + startDownload(); + } + }); + settingsList->addItem(downloadMapsButton); + + settingsList->addItem(lastMapsDownload = new LabelControl(tr("Last Updated"), params.get("LastMapsUpdate").empty() ? "Never" : QString::fromStdString(params.get("LastMapsUpdate")))); + + selectMaps = new FrogPilotButtonsControl(tr("Map Sources"), + tr("Select the countries or U.S. states to use with \"Speed Limit Controller\".") , + "", {tr("COUNTRIES"), tr("STATES")}); + QObject::connect(selectMaps, &FrogPilotButtonsControl::buttonClicked, [mapsLayout, this](int id) { + mapsLayout->setCurrentIndex(id + 1); + + openSubPanel(); + }); + settingsList->addItem(selectMaps); + + settingsList->addItem(downloadStatus = new LabelControl(tr("Progress"))); + settingsList->addItem(downloadTimeElapsed = new LabelControl(tr("Time Elapsed"))); + settingsList->addItem(downloadETA = new LabelControl(tr("Time Remaining"))); + + downloadETA->setVisible(false); + downloadStatus->setVisible(false); + downloadTimeElapsed->setVisible(false); + + removeMapsButton = new ButtonControl(tr("Remove Maps"), tr("REMOVE"), tr("Delete downloaded map data to free up storage space.")); + QObject::connect(removeMapsButton, &ButtonControl::clicked, [this] { + if (FrogPilotConfirmationDialog::yesorno(tr("Delete all downloaded maps?"), this)) { + std::thread([this] { + mapsSize->setText(tr("0 MB")); + + mapsFolderPath.removeRecursively(); + }).detach(); + } + }); + settingsList->addItem(removeMapsButton); + + resetMapdButton = new ButtonControl(tr("Reset Downloader"), tr("RESET"), + tr("Reset the map downloader. Use this if downloads are stuck or failing.")); + QObject::connect(resetMapdButton, &ButtonControl::clicked, [parent, this]() { + if (ConfirmationDialog::confirm(tr("Reset the map downloader? Your device will reboot afterward."), tr("Reset"), this)) { + std::thread([parent, this]() { + parent->keepScreenOn = true; + + resetMapdButton->setEnabled(false); + resetMapdButton->setValue(tr("Resetting...")); + + std::system("pkill mapd"); + + QDir("/data/media/0/osm").removeRecursively(); + + resetMapdButton->setValue(tr("Reset!")); + + util::sleep_for(2500); + + resetMapdButton->setValue(tr("Rebooting...")); + + util::sleep_for(2500); + + Hardware::reboot(); + }).detach(); + } + }); + settingsList->addItem(resetMapdButton); + + settingsList->addItem(mapsSize = new LabelControl(tr("Storage Used"), calculateDirectorySize(mapsFolderPath))); + + ScrollView *settingsPanel = new ScrollView(settingsList, this); + mapsLayout->addWidget(settingsPanel); + + FrogPilotListWidget *countriesList = new FrogPilotListWidget(this); + std::vector>> countries = { + {tr("Africa"), africaMap}, + {tr("Antarctica"), antarcticaMap}, + {tr("Asia"), asiaMap}, + {tr("Europe"), europeMap}, + {tr("North America"), northAmericaMap}, + {tr("Oceania"), oceaniaMap}, + {tr("South America"), southAmericaMap} + }; + + for (std::pair> country : countries) { + countriesList->addItem(new LabelControl(country.first, "")); + countriesList->addItem(new MapSelectionControl(country.second, true)); + } + + ScrollView *countryMapsPanel = new ScrollView(countriesList, this); + mapsLayout->addWidget(countryMapsPanel); + + FrogPilotListWidget *statesList = new FrogPilotListWidget(this); + std::vector>> states = { + {tr("United States - Midwest"), midwestMap}, + {tr("United States - Northeast"), northeastMap}, + {tr("United States - South"), southMap}, + {tr("United States - West"), westMap}, + {tr("United States - Territories"), territoriesMap} + }; + + for (std::pair> state : states) { + statesList->addItem(new LabelControl(state.first, "")); + statesList->addItem(new MapSelectionControl(state.second)); + } + + ScrollView *stateMapsPanel = new ScrollView(statesList, this); + mapsLayout->addWidget(stateMapsPanel); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [mapsLayout, settingsPanel, this] { + if (forceOpenDescriptions) { + downloadMapsButton->showDescription(); + preferredSchedule->showDescription(); + removeMapsButton->showDescription(); + resetMapdButton->showDescription(); + selectMaps->showDescription(); + } + + std::string mapsSelected = params.get("MapsSelected"); + hasMapsSelected = !QJsonDocument::fromJson(QByteArray::fromStdString(mapsSelected)).object().value("nations").toArray().isEmpty(); + hasMapsSelected |= !QJsonDocument::fromJson(QByteArray::fromStdString(mapsSelected)).object().value("states").toArray().isEmpty(); + + mapsLayout->setCurrentWidget(settingsPanel); + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotMapsPanel::updateState); +} + +void FrogPilotMapsPanel::showEvent(QShowEvent *event) { + if (forceOpenDescriptions) { + downloadMapsButton->showDescription(); + preferredSchedule->showDescription(); + removeMapsButton->showDescription(); + resetMapdButton->showDescription(); + selectMaps->showDescription(); + } + + UIState &s = *uiState(); + UIScene &scene = s.scene; + + FrogPilotUIState &fs = *frogpilotUIState(); + FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + std::string mapsSelected = params.get("MapsSelected"); + hasMapsSelected = !QJsonDocument::fromJson(QByteArray::fromStdString(mapsSelected)).object().value("nations").toArray().isEmpty(); + hasMapsSelected |= !QJsonDocument::fromJson(QByteArray::fromStdString(mapsSelected)).object().value("states").toArray().isEmpty(); + + bool parked = !scene.started || frogpilot_scene.parked || parent->isFrogsGoMoo; + + removeMapsButton->setVisible(mapsFolderPath.exists()); + + std::string osmDownloadProgress = params.get("OSMDownloadProgress"); + if (!osmDownloadProgress.empty()) { + downloadMapsButton->setText(tr("CANCEL")); + downloadStatus->setText(tr("Calculating...")); + + downloadStatus->setVisible(true); + + lastMapsDownload->setVisible(false); + removeMapsButton->setVisible(false); + resetMapdButton->setVisible(false); + + updateDownloadLabels(osmDownloadProgress); + } else { + downloadMapsButton->setEnabled(!cancellingDownload && hasMapsSelected && frogpilot_scene.online && parked); + downloadMapsButton->setValue(frogpilot_scene.online ? (parked ? "" : tr("Not parked")) : tr("Offline...")); + } +} + + +void FrogPilotMapsPanel::updateState(const UIState &s, const FrogPilotUIState &fs) { + if (!isVisible() || s.sm->frame % (UI_FREQ / 2) != 0) { + return; + } + + const FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + const UIScene &scene = s.scene; + + bool parked = !scene.started || frogpilot_scene.parked || parent->isFrogsGoMoo; + + std::string osmDownloadProgress = params.get("OSMDownloadProgress"); + if (!osmDownloadProgress.empty() && !cancellingDownload) { + updateDownloadLabels(osmDownloadProgress); + } else { + downloadMapsButton->setEnabled(!cancellingDownload && hasMapsSelected && frogpilot_scene.online && parked); + downloadMapsButton->setValue(frogpilot_scene.online ? (parked ? "" : tr("Not parked")) : tr("Offline...")); + } + + parent->keepScreenOn = !osmDownloadProgress.empty(); +} + +void FrogPilotMapsPanel::cancelDownload() { + cancellingDownload = true; + + downloadMapsButton->setEnabled(false); + + downloadETA->setText(tr("Calculating...")); + downloadMapsButton->setText(tr("CANCEL")); + downloadStatus->setText(tr("Calculating...")); + downloadTimeElapsed->setText(tr("Calculating...")); + + params.remove("OSMDownloadProgress"); + params_memory.remove("OSMDownloadLocations"); + + std::system("pkill mapd"); + + QTimer::singleShot(2500, [this]() { + cancellingDownload = false; + + downloadMapsButton->setEnabled(true); + + downloadMapsButton->setText(tr("DOWNLOAD")); + + downloadETA->setVisible(false); + downloadStatus->setVisible(false); + downloadTimeElapsed->setVisible(false); + + lastMapsDownload->setVisible(true); + removeMapsButton->setVisible(mapsFolderPath.exists()); + resetMapdButton->setVisible(true); + + update(); + }); +} + +void FrogPilotMapsPanel::startDownload() { + downloadETA->setText(tr("Calculating...")); + downloadMapsButton->setText(tr("CANCEL")); + downloadStatus->setText(tr("Calculating...")); + downloadTimeElapsed->setText(tr("Calculating...")); + + downloadETA->setVisible(true); + downloadStatus->setVisible(true); + downloadTimeElapsed->setVisible(true); + + lastMapsDownload->setVisible(false); + removeMapsButton->setVisible(false); + resetMapdButton->setVisible(false); + + elapsedTime.start(); + startTime = QDateTime::currentDateTime(); + + params_memory.put("OSMDownloadLocations", params.get("MapsSelected")); +} + +void FrogPilotMapsPanel::updateDownloadLabels(std::string &osmDownloadProgress) { + static std::regex fileStatusRegex(R"("total_files":(\d+),.*"downloaded_files":(\d+))"); + + std::smatch match; + if (std::regex_search(osmDownloadProgress, match, fileStatusRegex)) { + int totalFiles = std::stoi(match[1].str()); + int downloadedFiles = std::stoi(match[2].str()); + + if (downloadedFiles == totalFiles) { + downloadMapsButton->setText(tr("DOWNLOAD")); + lastMapsDownload->setText(formatCurrentDate()); + + downloadETA->setVisible(false); + downloadStatus->setVisible(false); + downloadTimeElapsed->setVisible(false); + + lastMapsDownload->setVisible(true); + removeMapsButton->setVisible(true); + resetMapdButton->setVisible(true); + + params.put("LastMapsUpdate", formatCurrentDate().toStdString()); + params.remove("OSMDownloadProgress"); + + update(); + + return; + } + + static int previousDownloadedFiles = 0; + if (downloadedFiles != previousDownloadedFiles) { + std::thread([this]() { + mapsSize->setText(calculateDirectorySize(mapsFolderPath)); + }).detach(); + } + + downloadETA->setText(QString("%1").arg(formatETA(elapsedTime.elapsed(), downloadedFiles, previousDownloadedFiles, totalFiles, startTime))); + downloadStatus->setText(QString("%1 / %2 (%3%)").arg(downloadedFiles).arg(totalFiles).arg((downloadedFiles * 100) / totalFiles)); + downloadTimeElapsed->setText(formatElapsedTime(elapsedTime.elapsed())); + + previousDownloadedFiles = downloadedFiles; + } +} diff --git a/frogpilot/ui/qt/offroad/maps_settings.h b/frogpilot/ui/qt/offroad/maps_settings.h new file mode 100644 index 000000000..8e3e91447 --- /dev/null +++ b/frogpilot/ui/qt/offroad/maps_settings.h @@ -0,0 +1,51 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" +#include "frogpilot/ui/qt/widgets/navigation_functions.h" + +class FrogPilotMapsPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotMapsPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void cancelDownload(); + void startDownload(); + void updateDownloadLabels(std::string &osmDownloadProgress); + void updateState(const UIState &s, const FrogPilotUIState &fs); + + bool cancellingDownload; + bool forceOpenDescriptions; + bool hasMapsSelected; + + ButtonControl *downloadMapsButton; + ButtonControl *removeMapsButton; + ButtonControl *resetMapdButton; + + ButtonParamControl *preferredSchedule; + + FrogPilotButtonsControl *selectMaps; + + FrogPilotSettingsWindow *parent; + + LabelControl *downloadETA; + LabelControl *downloadStatus; + LabelControl *downloadTimeElapsed; + LabelControl *lastMapsDownload; + LabelControl *mapsSize; + + Params params; + + QDateTime startTime; + + QDir mapsFolderPath{"/data/media/0/osm/offline"}; + + QElapsedTimer elapsedTime; +}; diff --git a/frogpilot/ui/qt/offroad/model_settings.cc b/frogpilot/ui/qt/offroad/model_settings.cc new file mode 100644 index 000000000..a42fae7cf --- /dev/null +++ b/frogpilot/ui/qt/offroad/model_settings.cc @@ -0,0 +1,611 @@ +#include "frogpilot/ui/qt/offroad/model_settings.h" + +bool hasAllTinygradFiles(const QDir &modelDir, const QString &modelKey) { + QStringList tinygradSuffixes = { + "_driving_policy_metadata.pkl", + "_driving_policy_tinygrad.pkl", + "_driving_vision_metadata.pkl", + "_driving_vision_tinygrad.pkl" + }; + + for (const QString &suffix : tinygradSuffixes) { + if (!modelDir.exists(modelKey + suffix)) { + return false; + } + } + return true; +} + +FrogPilotModelPanel::FrogPilotModelPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + defaultModel = QString::fromStdString(params.getKeyDefaultValue("DrivingModel").value()); + + QStackedLayout *modelLayout = new QStackedLayout(); + addItem(modelLayout); + + FrogPilotListWidget *modelList = new FrogPilotListWidget(this); + + ScrollView *modelPanel = new ScrollView(modelList, this); + + modelLayout->addWidget(modelPanel); + + FrogPilotListWidget *modelLabelsList = new FrogPilotListWidget(this); + + ScrollView *modelLabelsPanel = new ScrollView(modelLabelsList, this); + + modelLayout->addWidget(modelLabelsPanel); + + const std::vector> modelToggles { + {"AutomaticallyDownloadModels", tr("Automatically Download New Models"), tr("Automatically download new driving models as they become available."), ""}, + {"DeleteModel", tr("Delete Driving Models"), tr("Delete downloaded driving models to free up storage space."), ""}, + {"DownloadModel", tr("Download Driving Models"), tr("Manually download driving models to the device."), ""}, + {"ModelRandomizer", tr("Model Randomizer"), tr("Select a random driving model each drive and use feedback prompts at the end of the drive to help find the model that best suits you!"), ""}, + {"ManageBlacklistedModels", tr("Manage Model Blacklist"), tr("Add or remove driving models from the \"Model Randomizer\" blacklist."), ""}, + {"ManageScores", tr("Manage Model Ratings"), tr("View or reset saved model ratings used by the \"Model Randomizer\"."), ""}, + {"SelectModel", tr("Select Driving Model"), tr("Choose which driving model openpilot uses."), ""}, + {"UpdateTinygrad", tr("Update Model Manager"), tr("Update the \"Model Manager\" to support the latest models."), ""} + }; + + for (const auto &[param, title, desc, icon] : modelToggles) { + AbstractControl *modelToggle; + + if (param == "DeleteModel") { + deleteModelButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DELETE ALL")}); + QObject::connect(deleteModelButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList deletableModels; + for (const QString &file : modelDir.entryList(QDir::Files)) { + QString base = QFileInfo(file).baseName(); + for (const QString &modelKey : modelFileToNameMapProcessed.keys()) { + if (base.startsWith(modelKey)) { + QString modelName = modelFileToNameMapProcessed.value(modelKey); + if (!deletableModels.contains(modelName)) { + deletableModels.append(modelName); + } + } + } + } + deletableModels.removeAll(cleanModelName(currentModel)); + deletableModels.removeAll(modelFileToNameMapProcessed.value(cleanModelName(defaultModel))); + noModelsDownloaded = deletableModels.isEmpty(); + + if (id == 0) { + QString modelToDelete = MultiOptionDialog::getSelection(tr("Select a driving model to delete"), deletableModels, "", this); + if (!modelToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Are you sure you want to delete the \"%1\" model?").arg(modelToDelete), tr("Delete"), this)) { + QString modelFile = modelFileToNameMapProcessed.key(modelToDelete); + for (const QString &file : modelDir.entryList(QDir::Files)) { + QString base = QFileInfo(file).baseName(); + if (base.startsWith(modelFile)) { + QFile::remove(modelDir.filePath(file)); + } + } + + noModelsDownloaded = deletableModels.isEmpty(); + + deletableModels.removeAll(modelToDelete); + deleteModelButton->setEnabled(!noModelsDownloaded); + + allModelsDownloaded = false; + } + } else if (id == 1) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to delete all of your downloaded driving models?"), tr("Delete"), this)) { + for (const QString &file : modelDir.entryList(QDir::Files)) { + QString base = QFileInfo(file).baseName(); + for (const QString &modelKey : modelFileToNameMapProcessed.keys()) { + QString modelName = modelFileToNameMapProcessed.value(modelKey); + if (deletableModels.contains(modelName) && base.startsWith(modelKey)) { + QFile::remove(modelDir.filePath(file)); + break; + } + } + } + + deleteModelButton->setEnabled(false); + + allModelsDownloaded = false; + noModelsDownloaded = true; + } + } + }); + modelToggle = deleteModelButton; + } else if (param == "DownloadModel") { + downloadModelButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DOWNLOAD"), tr("DOWNLOAD ALL")}); + QObject::connect(downloadModelButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + if (tinygradUpdate) { + if (FrogPilotConfirmationDialog::yesorno(tr("Tinygrad is out of date and must be updated before you can download new models. Update now?"), this)) { + if (FrogPilotConfirmationDialog::yesorno(tr("Updating Tinygrad will delete all existing Tinygrad-based models which will need to be re-downloaded. Proceed?"), this)) { + params_memory.putBool("UpdateTinygrad", true); + params_memory.put("ModelDownloadProgress", "Downloading..."); + + updateTinygradButton->setText(0, tr("CANCEL")); + updateTinygradButton->setValue(tr("Updating...")); + + updatingTinygrad = true; + } + } + } else if (id == 0) { + if (modelDownloading) { + params_memory.putBool("CancelModelDownload", true); + + cancellingDownload = true; + } else { + QStringList downloadableModels = availableModelNames; + for (const QString &modelKey : modelFileToNameMap.keys()) { + QString modelName = modelFileToNameMap.value(modelKey); + if (hasAllTinygradFiles(modelDir, modelKey)) { + downloadableModels.removeAll(modelName); + } + } + allModelsDownloaded = downloadableModels.isEmpty(); + + QString modelToDownload = MultiOptionDialog::getSelection(tr("Select a driving model to download"), downloadableModels, "", this); + if (!modelToDownload.isEmpty()) { + params_memory.put("ModelToDownload", modelFileToNameMap.key(modelToDownload).toStdString()); + params_memory.put("ModelDownloadProgress", "Downloading..."); + + downloadModelButton->setText(0, tr("CANCEL")); + + downloadModelButton->setValue(tr("Downloading...")); + + downloadModelButton->setVisibleButton(1, false); + + modelDownloading = true; + } + } + } else if (id == 1) { + if (allModelsDownloading) { + params_memory.putBool("CancelModelDownload", true); + + cancellingDownload = true; + } else { + params_memory.putBool("DownloadAllModels", true); + params_memory.put("ModelDownloadProgress", "Downloading..."); + + downloadModelButton->setText(1, tr("CANCEL")); + + downloadModelButton->setValue(tr("Downloading...")); + + downloadModelButton->setVisibleButton(0, false); + + allModelsDownloading = true; + } + } + }); + modelToggle = downloadModelButton; + } else if (param == "ManageBlacklistedModels") { + FrogPilotButtonsControl *blacklistButton = new FrogPilotButtonsControl(title, desc, icon, {tr("ADD"), tr("REMOVE"), tr("REMOVE ALL")}); + QObject::connect(blacklistButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList blacklistedModels = QString::fromStdString(params.get("BlacklistedModels")).split(","); + blacklistedModels.removeAll(""); + + if (id == 0) { + QStringList blacklistableModels; + for (const QString &model : modelFileToNameMapProcessed.keys()) { + if (!blacklistedModels.contains(model)) { + blacklistableModels.append(modelFileToNameMapProcessed.value(model)); + } + } + + if (blacklistableModels.size() <= 1) { + ConfirmationDialog::alert(tr("There are no more driving models to blacklist. The only available model is \"%1\"!").arg(blacklistableModels.first()), this); + } else { + QString modelToBlacklist = MultiOptionDialog::getSelection(tr("Select a driving model to add to the blacklist"), blacklistableModels, "", this); + if (!modelToBlacklist.isEmpty()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to add the \"%1\" model to the blacklist?").arg(modelToBlacklist), tr("Add"), this)) { + blacklistedModels.append(modelFileToNameMapProcessed.key(modelToBlacklist)); + + params.put("BlacklistedModels", blacklistedModels.join(",").toStdString()); + } + } + } + } else if (id == 1) { + QStringList whitelistableModels; + for (const QString &model : blacklistedModels) { + QString modelName = modelFileToNameMapProcessed.value(model); + whitelistableModels.append(modelName); + } + whitelistableModels.sort(); + + QString modelToWhitelist = MultiOptionDialog::getSelection(tr("Select a driving model to remove from the blacklist"), whitelistableModels, "", this); + if (!modelToWhitelist.isEmpty()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to remove the \"%1\" model from the blacklist?").arg(modelToWhitelist), tr("Remove"), this)) { + blacklistedModels.removeAll(modelFileToNameMapProcessed.key(modelToWhitelist)); + + params.put("BlacklistedModels", blacklistedModels.join(",").toStdString()); + } + } + } else if (id == 2) { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to remove all of your blacklisted driving models?"), this)) { + params.remove("BlacklistedModels"); + } + } + }); + modelToggle = blacklistButton; + } else if (param == "ManageScores") { + FrogPilotButtonsControl *manageScoresButton = new FrogPilotButtonsControl(title, desc, icon, {tr("RESET"), tr("VIEW")}); + QObject::connect(manageScoresButton, &FrogPilotButtonsControl::buttonClicked, [modelLayout, modelLabelsList, modelLabelsPanel, this](int id) { + if (id == 0) { + if (FrogPilotConfirmationDialog::yesorno(tr("Reset all model drives and ratings? This clears your drive history and collected feedback!"), this)) { + params.remove("ModelDrivesAndScores"); + } + } else if (id == 1) { + openSubPanel(); + + updateModelLabels(modelLabelsList); + + modelLayout->setCurrentWidget(modelLabelsPanel); + } + }); + modelToggle = manageScoresButton; + } else if (param == "SelectModel") { + selectModelButton = new ButtonControl(title, tr("SELECT"), desc); + QObject::connect(selectModelButton, &ButtonControl::clicked, [this]() { + QStringList selectableModels; + for (const QString &modelKey : modelFileToNameMap.keys()) { + if (modelKey != cleanModelName(defaultModel) && hasAllTinygradFiles(modelDir, modelKey)) { + selectableModels.append(modelFileToNameMap.value(modelKey)); + } + } + selectableModels.sort(); + selectableModels.prepend(modelFileToNameMap.value(cleanModelName(defaultModel)) + " (Default)"); + + QString currentModelDisplay = currentModel; + if (currentModel == modelFileToNameMap.value(cleanModelName(defaultModel))) { + currentModelDisplay += " (Default)"; + } + + QString modelToSelect = MultiOptionDialog::getSelection(tr("Select a Model"), selectableModels, currentModelDisplay, this); + if (!modelToSelect.isEmpty()) { + modelToSelect.remove(" (Default)"); + currentModel = modelToSelect; + + params.put("DrivingModel", modelFileToNameMap.key(modelToSelect).toStdString()); + + updateFrogPilotToggles(); + + if (started) { + if (FrogPilotConfirmationDialog::toggleReboot(this)) { + Hardware::reboot(); + } + } + selectModelButton->setValue(modelToSelect); + + QStringList deletableModels; + for (const QString &file : modelDir.entryList(QDir::Files)) { + QString base = QFileInfo(file).baseName(); + for (const QString &modelKey : modelFileToNameMapProcessed.keys()) { + if (base.startsWith(modelKey)) { + QString modelName = modelFileToNameMapProcessed.value(modelKey); + if (!deletableModels.contains(modelName)) { + deletableModels.append(modelName); + } + } + } + } + deletableModels.removeAll(cleanModelName(currentModel)); + deletableModels.removeAll(modelFileToNameMapProcessed.value(cleanModelName(defaultModel))); + noModelsDownloaded = deletableModels.isEmpty(); + } + }); + modelToggle = selectModelButton; + + } else if (param == "UpdateTinygrad") { + updateTinygradButton = new FrogPilotButtonsControl(title, desc, icon, {tr("UPDATE")}); + QObject::connect(updateTinygradButton, &FrogPilotButtonsControl::buttonClicked, [this]() { + if (updatingTinygrad) { + params_memory.putBool("CancelModelDownload", true); + + updateTinygradButton->setEnabled(false); + updateTinygradButton->setValue(tr("Cancelling...")); + + cancellingDownload = true; + } else { + if (FrogPilotConfirmationDialog::yesorno(tr("Updating Tinygrad will delete existing Tinygrad-based driving models and need to be re-downloaded. Proceed?"), this)) { + params_memory.putBool("UpdateTinygrad", true); + params_memory.put("ModelDownloadProgress", "Downloading..."); + + updateTinygradButton->setText(0, tr("CANCEL")); + updateTinygradButton->setValue(tr("Updating...")); + + updatingTinygrad = true; + } + } + }); + modelToggle = updateTinygradButton; + + } else { + modelToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = modelToggle; + + modelList->addItem(modelToggle); + + QObject::connect(modelToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(modelToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + openDescriptions(forceOpenDescriptions, toggles); + + QObject::connect(static_cast(toggles["ModelRandomizer"]), &ToggleControl::toggleFlipped, [this](bool state) { + updateToggles(); + + if (state && !allModelsDownloaded) { + if (FrogPilotConfirmationDialog::yesorno(tr("The \"Model Randomizer\" works only with downloaded models. Download all models now?"), this)) { + params_memory.putBool("DownloadAllModels", true); + params_memory.put("ModelDownloadProgress", "Downloading..."); + + downloadModelButton->setValue(tr("Downloading...")); + + allModelsDownloading = true; + } + } + }); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [modelLayout, modelPanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + modelLayout->setCurrentWidget(modelPanel); + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotModelPanel::updateState); +} + +void FrogPilotModelPanel::showEvent(QShowEvent *event) { + UIState &s = *uiState(); + + FrogPilotUIState &fs = *frogpilotUIState(); + FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + allModelsDownloading = params_memory.getBool("DownloadAllModels"); + modelDownloading = !params_memory.get("ModelDownloadProgress").empty(); + tinygradUpdate = params.getBool("TinygradUpdateAvailable"); + updatingTinygrad = params_memory.getBool("UpdateTinygrad"); + + modelDownloading &= !updatingTinygrad; + + QStringList availableModels = QString::fromStdString(params.get("AvailableModels")).split(","); + availableModels.sort(); + availableModelNames = QString::fromStdString(params.get("AvailableModelNames")).split(","); + availableModelNames.sort(); + + modelFileToNameMap.clear(); + modelFileToNameMapProcessed.clear(); + for (int i = 0; i < qMin(availableModels.size(), availableModelNames.size()); ++i) { + modelFileToNameMap.insert(availableModels[i], availableModelNames[i]); + modelFileToNameMapProcessed.insert(availableModels[i], cleanModelName(availableModelNames[i])); + } + + QStringList downloadableModels = availableModelNames; + for (const QString &modelKey : modelFileToNameMap.keys()) { + QString modelName = modelFileToNameMap.value(modelKey); + if (hasAllTinygradFiles(modelDir, modelKey)) { + downloadableModels.removeAll(modelName); + } + } + allModelsDownloaded = downloadableModels.isEmpty(); + + QStringList deletableModels; + for (const QString &file : modelDir.entryList(QDir::Files)) { + QString base = QFileInfo(file).baseName(); + for (const QString &modelKey : modelFileToNameMapProcessed.keys()) { + if (base.startsWith(modelKey)) { + QString modelName = modelFileToNameMapProcessed.value(modelKey); + if (!deletableModels.contains(modelName)) { + deletableModels.append(modelName); + } + } + } + } + deletableModels.removeAll(cleanModelName(currentModel)); + deletableModels.removeAll(modelFileToNameMapProcessed.value(cleanModelName(defaultModel))); + noModelsDownloaded = deletableModels.isEmpty(); + + QString modelKey = cleanModelName(QString::fromStdString(params.get("DrivingModel"))); + if (!hasAllTinygradFiles(modelDir, modelKey)) { + modelKey = defaultModel; + } + currentModel = modelFileToNameMap.value(modelKey); + + QString currentModelName = currentModel; + if (modelKey == defaultModel) { + currentModelName.append(" (Default)"); + } + selectModelButton->setValue(currentModelName); + + bool parked = !s.scene.started || frogpilot_scene.parked || parent->isFrogsGoMoo; + + deleteModelButton->setEnabled(!(allModelsDownloading || modelDownloading || noModelsDownloaded)); + + downloadModelButton->setEnabledButtons(0, !allModelsDownloaded && !allModelsDownloading && !cancellingDownload && !updatingTinygrad && frogpilot_scene.online && parked); + downloadModelButton->setEnabledButtons(1, !allModelsDownloaded && !modelDownloading && !cancellingDownload && !updatingTinygrad && frogpilot_scene.online && parked); + + downloadModelButton->setValue(frogpilot_scene.online ? (parked ? "" : "Not parked") : tr("Offline...")); + + updateTinygradButton->setEnabled(!modelDownloading && !cancellingDownload && frogpilot_scene.online && parked && tinygradUpdate); + updateTinygradButton->setValue(tinygradUpdate ? tr("Update available!") : tr("Up to date!")); + + started = s.scene.started; + + updateToggles(); +} + +void FrogPilotModelPanel::updateState(const UIState &s, const FrogPilotUIState &fs) { + if (!isVisible() || finalizingDownload) { + return; + } + + const FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + bool parked = !started || frogpilot_scene.parked || parent->isFrogsGoMoo; + + if (allModelsDownloading || modelDownloading) { + QString progress = QString::fromStdString(params_memory.get("ModelDownloadProgress")); + bool downloadFailed = progress.contains(QRegularExpression("cancelled|exists|failed|missing|offline", QRegularExpression::CaseInsensitiveOption)); + + { + QString translatedProgress; + if (progress == "Downloading...") { + translatedProgress = tr("Downloading..."); + } else if (progress == "Downloaded!") { + translatedProgress = tr("Downloaded!"); + } else if (progress == "All models downloaded!") { + translatedProgress = tr("All models downloaded!"); + } else if (progress.contains("cancelled", Qt::CaseInsensitive)) { + translatedProgress = tr("Download cancelled..."); + } else if (progress.contains("failed", Qt::CaseInsensitive)) { + translatedProgress = tr("Download failed..."); + } else if (progress.contains("offline", Qt::CaseInsensitive)) { + translatedProgress = tr("GitHub and GitLab are offline..."); + } else if (progress == "Repository unavailable") { + translatedProgress = tr("Repository unavailable"); + } else { + translatedProgress = progress; + } + downloadModelButton->setValue(translatedProgress); + } + + if (progress == "All models downloaded!" || progress == "Downloaded!" && !allModelsDownloading || downloadFailed) { + finalizingDownload = true; + + QTimer::singleShot(2500, [progress, this]() { + allModelsDownloading = false; + cancellingDownload = false; + finalizingDownload = false; + modelDownloading = false; + noModelsDownloaded = false; + + QStringList downloadableModels = availableModelNames; + for (const QString &modelKey : modelFileToNameMap.keys()) { + QString modelName = modelFileToNameMap.value(modelKey); + if (hasAllTinygradFiles(modelDir, modelKey)) { + downloadableModels.removeAll(modelName); + } + } + allModelsDownloaded = downloadableModels.isEmpty(); + + params_memory.remove("ModelDownloadProgress"); + + downloadModelButton->setEnabled(true); + downloadModelButton->setValue(""); + }); + } + } else { + downloadModelButton->setValue(frogpilot_scene.online ? (parked ? "" : "Not parked") : tr("Offline...")); + } + + if (updatingTinygrad) { + QString progress = QString::fromStdString(params_memory.get("ModelDownloadProgress")); + bool downloadFailed = progress.contains(QRegularExpression("cancelled|exists|failed|missing|offline", QRegularExpression::CaseInsensitiveOption)); + + { + QString translatedProgress; + if (progress == "Downloading...") { + translatedProgress = tr("Downloading..."); + } else if (progress == "Downloaded!") { + translatedProgress = tr("Downloaded!"); + } else if (progress == "All models downloaded!") { + translatedProgress = tr("All models downloaded!"); + } else if (progress.contains("cancelled", Qt::CaseInsensitive)) { + translatedProgress = tr("Download cancelled..."); + } else if (progress.contains("failed", Qt::CaseInsensitive)) { + translatedProgress = tr("Download failed..."); + } else if (progress.contains("offline", Qt::CaseInsensitive)) { + translatedProgress = tr("GitHub and GitLab are offline..."); + } else if (progress == "Repository unavailable") { + translatedProgress = tr("Repository unavailable"); + } else { + translatedProgress = progress; + } + updateTinygradButton->setValue(translatedProgress); + } + + if (progress == "Updated!" && updatingTinygrad || downloadFailed) { + finalizingDownload = true; + + QTimer::singleShot(2500, [progress, this]() { + modelDownloading = !params_memory.get("ModelDownloadProgress").empty(); + + if (modelDownloading) { + downloadModelButton->setText(1, tr("CANCEL")); + + downloadModelButton->setValue(tr("Downloading...")); + + downloadModelButton->setVisibleButton(0, false); + } else { + cancellingDownload = false; + } + + tinygradUpdate = params.getBool("TinygradUpdateAvailable"); + + finalizingDownload = false; + updatingTinygrad = false; + + updateTinygradButton->setEnabled(tinygradUpdate); + updateTinygradButton->setText(0, tr("UPDATE")); + updateTinygradButton->setValue(tinygradUpdate ? tr("Update available!") : tr("Up to date!")); + }); + } + } + + deleteModelButton->setEnabled(!(allModelsDownloading || modelDownloading || noModelsDownloaded)); + + downloadModelButton->setText(0, modelDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + downloadModelButton->setText(1, allModelsDownloading ? tr("CANCEL") : tr("DOWNLOAD ALL")); + + downloadModelButton->setEnabledButtons(0, !allModelsDownloaded && !allModelsDownloading && !cancellingDownload && !finalizingDownload && !updatingTinygrad && frogpilot_scene.online && parked); + downloadModelButton->setEnabledButtons(1, !allModelsDownloaded && !modelDownloading && !cancellingDownload && !finalizingDownload && !updatingTinygrad && frogpilot_scene.online && parked); + + downloadModelButton->setVisibleButton(0, !allModelsDownloading); + downloadModelButton->setVisibleButton(1, !modelDownloading); + + updateTinygradButton->setEnabled(!modelDownloading && !cancellingDownload && !cancellingDownload && !finalizingDownload && frogpilot_scene.online && parked && tinygradUpdate); + + started = s.scene.started; + + parent->keepScreenOn = allModelsDownloading || modelDownloading || updatingTinygrad; +} + +void FrogPilotModelPanel::updateModelLabels(FrogPilotListWidget *labelsList) { + labelsList->clear(); + + QJsonObject modelDrivesAndScores = QJsonDocument::fromJson(QString::fromStdString(params.get("ModelDrivesAndScores")).toUtf8()).object(); + + for (const QString &modelName : availableModelNames) { + QJsonObject modelData = modelDrivesAndScores.value(cleanModelName(modelName)).toObject(); + + int drives = modelData.value("Drives").toInt(0); + int score = modelData.value("Score").toInt(0); + + QString drivesDisplay = drives == 1 ? QString("%1 Drive").arg(drives) : drives > 0 ? QString("%1 Drives").arg(drives) : "N/A"; + QString scoreDisplay = drives > 0 ? QString("Score: %1%").arg(score) : "N/A"; + + QString labelTitle = cleanModelName(modelName); + QString labelText = QString("%1 (%2)").arg(scoreDisplay, drivesDisplay); + + LabelControl *labelControl = new LabelControl(labelTitle, labelText, "", this); + labelsList->addItem(labelControl); + } +} + +void FrogPilotModelPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "ManageBlacklistedModels" || key == "ManageScores") { + setVisible &= params.getBool("ModelRandomizer"); + } + + else if (key == "SelectModel") { + setVisible &= !params.getBool("ModelRandomizer"); + } + + toggle->setVisible(setVisible); + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/model_settings.h b/frogpilot/ui/qt/offroad/model_settings.h new file mode 100644 index 000000000..d35370e0d --- /dev/null +++ b/frogpilot/ui/qt/offroad/model_settings.h @@ -0,0 +1,54 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotModelPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotModelPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateModelLabels(FrogPilotListWidget *labelsList); + void updateState(const UIState &s, const FrogPilotUIState &fs); + void updateToggles(); + + bool allModelsDownloaded; + bool allModelsDownloading; + bool cancellingDownload; + bool finalizingDownload; + bool forceOpenDescriptions; + bool modelDownloading; + bool noModelsDownloaded; + bool started; + bool tinygradUpdate; + bool updatingTinygrad; + + std::map toggles; + + ButtonControl *selectModelButton; + + FrogPilotButtonsControl *deleteModelButton; + FrogPilotButtonsControl *downloadModelButton; + FrogPilotButtonsControl *updateTinygradButton; + + FrogPilotSettingsWindow *parent; + + Params params; + + QDir modelDir{"/data/models/"}; + + QMap modelFileToNameMap; + QMap modelFileToNameMapProcessed; + + QString currentModel; + QString defaultModel; + + QStringList availableModelNames; +}; diff --git a/frogpilot/ui/qt/offroad/navigation_settings.cc b/frogpilot/ui/qt/offroad/navigation_settings.cc new file mode 100644 index 000000000..4f787858c --- /dev/null +++ b/frogpilot/ui/qt/offroad/navigation_settings.cc @@ -0,0 +1,330 @@ +#include "frogpilot/ui/qt/offroad/navigation_settings.h" + +FrogPilotNavigationPanel::FrogPilotNavigationPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + networkManager = new QNetworkAccessManager(this); + + primelessLayout = new QStackedLayout(); + addItem(primelessLayout); + + FrogPilotListWidget *settingsList = new FrogPilotListWidget(this); + ipLabel = new LabelControl(tr("Manage Your Settings At"), tr("Offline...")); + settingsList->addItem(ipLabel); + + publicMapboxKeyControl = new FrogPilotButtonsControl(tr("Public Mapbox Key"), tr("Manage your Public Mapbox Key."), "", {tr("ADD"), tr("TEST")}); + QObject::connect(publicMapboxKeyControl, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + if (id == 0) { + if (mapboxPublicKeySet) { + if (FrogPilotConfirmationDialog::yesorno(tr("Remove your Public Mapbox Key?"), this)) { + params.remove("MapboxPublicKey"); + + updateButtons(); + } + } else { + int minKeyLength = 80; + QString key = InputDialog::getText(tr("Enter your Public Mapbox Key"), this, "", false, minKeyLength).trimmed(); + if (!key.isEmpty()) { + if (!key.startsWith("pk.")) { + key = "pk." + key; + } + params.put("MapboxPublicKey", key.toStdString()); + updateButtons(); + } + } + } else { + publicMapboxKeyControl->setValue(tr("Testing...")); + + QString key = QString::fromStdString(params.get("MapboxPublicKey")); + QString url = QString("https://api.mapbox.com/geocoding/v5/mapbox.places/mapbox.json?access_token=%1").arg(key); + + QNetworkRequest request(url); + QNetworkReply *reply = networkManager->get(request); + connect(reply, &QNetworkReply::finished, [=]() { + publicMapboxKeyControl->setValue(""); + + QString message; + if (reply->error() == QNetworkReply::NoError) { + message = tr("Key is valid!"); + } else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401) { + message = tr("Key is invalid!"); + } else { + message = tr("An error occurred: %1").arg(reply->errorString()); + } + ConfirmationDialog::alert(message, this); + reply->deleteLater(); + }); + } + }); + settingsList->addItem(publicMapboxKeyControl); + + secretMapboxKeyControl = new FrogPilotButtonsControl(tr("Secret Mapbox Key"), tr("Manage your Secret Mapbox Key."), "", {tr("ADD"), tr("TEST")}); + QObject::connect(secretMapboxKeyControl, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + if (id == 0) { + if (mapboxSecretKeySet) { + if (FrogPilotConfirmationDialog::yesorno(tr("Remove your Secret Mapbox Key?"), this)) { + params.remove("MapboxSecretKey"); + + updateButtons(); + } + } else { + int minKeyLength = 80; + QString key = InputDialog::getText(tr("Enter your Secret Mapbox Key"), this, "", false, minKeyLength).trimmed(); + if (!key.isEmpty()) { + if (!key.startsWith("sk.")) { + key = "sk." + key; + } + params.put("MapboxSecretKey", key.toStdString()); + updateButtons(); + } + } + } else { + secretMapboxKeyControl->setValue(tr("Testing...")); + + QString key = QString::fromStdString(params.get("MapboxSecretKey")); + QString url = QString("https://api.mapbox.com/directions/v5/mapbox/driving/-73.989,40.733;-74,40.733?access_token=%1").arg(key); + + QNetworkRequest request(url); + QNetworkReply *reply = networkManager->get(request); + connect(reply, &QNetworkReply::finished, [=]() { + secretMapboxKeyControl->setValue(""); + + QString message; + if (reply->error() == QNetworkReply::NoError) { + message = tr("Key is valid!"); + } else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401) { + message = tr("Key is invalid!"); + } else { + message = tr("An error occurred: %1").arg(reply->errorString()); + } + ConfirmationDialog::alert(message, this); + reply->deleteLater(); + }); + } + }); + settingsList->addItem(secretMapboxKeyControl); + + setupButton = new ButtonControl(tr("Mapbox Setup Instructions"), tr("VIEW"), tr("Instructions on how to set up Mapbox for \"Primeless Navigation\"."), this); + QObject::connect(setupButton, &ButtonControl::clicked, [this]() { + openSubPanel(); + + updateStep(); + + primelessLayout->setCurrentIndex(1); + }); + settingsList->addItem(setupButton); + + std::vector filterButtonNames{tr("CANCEL"), tr("Manually Update Speed Limits")}; + updateSpeedLimitsToggle = new FrogPilotButtonControl("SpeedLimitFiller", tr("Speed Limit Filler"), + tr("Automatically collect missing or incorrect speed limits while you drive using speeds limits sourced from your dashboard (if supported), " + "Mapbox, and \"Navigate on openpilot\".

" + "When you're parked and connected to Wi-Fi, FrogPilot will automatically processes this data into a file " + "to be used with the tool located at \"SpeedLimitFiller.frogpilot.com\".

" + "You can download this file from \"The Pond\" in the \"Download Speed Limits\" menu.

" + "Need a step-by-step guide? Visit #speed-limit-filler in the FrogPilot Discord!"), + "", filterButtonNames); + QObject::connect(updateSpeedLimitsToggle, &FrogPilotButtonControl::buttonClicked, [this](int id) { + if (id == 0) { + if (FrogPilotConfirmationDialog::yesorno(tr("Cancel the speed-limit update?"), this)) { + updatingLimits = false; + + updateSpeedLimitsToggle->setEnabledButton(0, false); + updateSpeedLimitsToggle->setValue(tr("Cancelled...")); + + params_memory.remove("UpdateSpeedLimits"); + + QTimer::singleShot(2500, [this]() { + updateSpeedLimitsToggle->clearCheckedButtons(); + updateSpeedLimitsToggle->setEnabledButton(0, true); + updateSpeedLimitsToggle->setValue(""); + updateSpeedLimitsToggle->setVisibleButton(0, false); + updateSpeedLimitsToggle->setVisibleButton(1, true); + + params_memory.remove("UpdateSpeedLimitsStatus"); + }); + } + } else if (id == 1) { + QJsonObject overpassRequests = QJsonDocument::fromJson(QString::fromStdString(params.get("OverpassRequests")).toUtf8()).object(); + + int totalRequests = overpassRequests.value("total_requests").toInt(0); + int maxRequests = overpassRequests.value("max_requests").toInt(10000); + int savedDay = overpassRequests.value("day").toInt(QDate::currentDate().day()); + + int currentDay = QDate::currentDate().day(); + + if (savedDay != currentDay) { + totalRequests = 0; + } + + if (totalRequests >= maxRequests) { + QTime now = QTime::currentTime(); + + int secondsUntilMidnight = (24 * 3600) - (now.hour() * 3600 + now.minute() * 60 + now.second()); + int hours = secondsUntilMidnight / 3600; + int minutes = (secondsUntilMidnight % 3600) / 60; + + ConfirmationDialog::alert(QString(tr("You've hit today's request limit.\n\nIt will reset in %1 hours and %2 minutes.")).arg(hours).arg(minutes), this); + + updateSpeedLimitsToggle->clearCheckedButtons(); + return; + } + + updateSpeedLimitsToggle->setVisibleButton(0, true); + updateSpeedLimitsToggle->setVisibleButton(1, false); + + if (FrogPilotConfirmationDialog::yesorno(tr("This process takes a while. It's recommended to start when you're done driving and connected to stable Wi-Fi. Continue?"), this)) { + updatingLimits = true; + + updateSpeedLimitsToggle->setValue("Calculating..."); + + params_memory.put("UpdateSpeedLimitsStatus", "Calculating..."); + params_memory.putBool("UpdateSpeedLimits", true); + } else { + updateSpeedLimitsToggle->setVisibleButton(0, false); + updateSpeedLimitsToggle->setVisibleButton(1, true); + + updateSpeedLimitsToggle->clearCheckedButtons(); + } + } + }); + updateSpeedLimitsToggle->setVisibleButton(0, false); + settingsList->addItem(updateSpeedLimitsToggle); + + ScrollView *settingsPanel = new ScrollView(settingsList, this); + primelessLayout->addWidget(settingsPanel); + + imageLabel = new QLabel(this); + + ScrollView *instructionsPanel = new ScrollView(imageLabel, this); + primelessLayout->addWidget(instructionsPanel); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [this]() { + primelessLayout->setCurrentIndex(0); + + if (forceOpenDescriptions) { + publicMapboxKeyControl->showDescription(); + secretMapboxKeyControl->showDescription(); + setupButton->showDescription(); + updateSpeedLimitsToggle->showDescription(); + } + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotNavigationPanel::updateState); +} + +void FrogPilotNavigationPanel::showEvent(QShowEvent *event) { + if (forceOpenDescriptions) { + publicMapboxKeyControl->showDescription(); + secretMapboxKeyControl->showDescription(); + setupButton->showDescription(); + updateSpeedLimitsToggle->showDescription(); + } + + UIState &s = *uiState(); + + FrogPilotUIState &fs = *frogpilotUIState(); + FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + QString ipAddress = fs.wifi->getIp4Address(); + ipLabel->setText(ipAddress.isEmpty() ? tr("Offline...") : QString("%1:8082").arg(ipAddress)); + + updateButtons(); + + updatingLimits = !params_memory.get("UpdateSpeedLimitsStatus").empty() && QString::fromStdString(params_memory.get("UpdateSpeedLimitsStatus")) != "Completed!"; + + bool parked = !s.scene.started || frogpilot_scene.parked || parent->isFrogsGoMoo; + + updateSpeedLimitsToggle->setVisibleButton(0, updatingLimits); + updateSpeedLimitsToggle->setVisibleButton(1, !updatingLimits); + + if (updatingLimits) { + updateSpeedLimitsToggle->setValue(QString::fromStdString(params_memory.get("UpdateSpeedLimitsStatus"))); + } else { + updateSpeedLimitsToggle->setEnabledButton(1, frogpilot_scene.online && util::system_time_valid() && parked); + updateSpeedLimitsToggle->setValue(frogpilot_scene.online ? (parked ? "" : "Not parked") : tr("Offline...")); + updateSpeedLimitsToggle->setVisible(parent->tuningLevel >= parent->frogpilotToggleLevels["SpeedLimitFiller"].toDouble()); + } +} + +void FrogPilotNavigationPanel::hideEvent(QHideEvent *event) { + primelessLayout->setCurrentIndex(0); +} + +void FrogPilotNavigationPanel::mousePressEvent(QMouseEvent *event) { + if (primelessLayout->currentIndex() == 1) { + closeSubPanel(); + + primelessLayout->setCurrentIndex(0); + + if (forceOpenDescriptions) { + publicMapboxKeyControl->showDescription(); + secretMapboxKeyControl->showDescription(); + setupButton->showDescription(); + updateSpeedLimitsToggle->showDescription(); + } + } +} + +void FrogPilotNavigationPanel::updateButtons() { + FrogPilotUIState &fs = *frogpilotUIState(); + FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + mapboxPublicKeySet = QString::fromStdString(params.get("MapboxPublicKey")).startsWith("pk"); + mapboxSecretKeySet = QString::fromStdString(params.get("MapboxSecretKey")).startsWith("sk"); + + publicMapboxKeyControl->setText(0, mapboxPublicKeySet ? tr("REMOVE") : tr("ADD")); + publicMapboxKeyControl->setVisibleButton(1, mapboxPublicKeySet && frogpilot_scene.online); + secretMapboxKeyControl->setText(0, mapboxSecretKeySet ? tr("REMOVE") : tr("ADD")); + secretMapboxKeyControl->setVisibleButton(1, mapboxSecretKeySet && frogpilot_scene.online); +} + +void FrogPilotNavigationPanel::updateState(const UIState &s, const FrogPilotUIState &fs) { + if (!isVisible() || s.sm->frame % (UI_FREQ / 2) != 0) { + return; + } + + const FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + updateButtons(); + updateStep(); + + bool parked = !s.scene.started || frogpilot_scene.parked || parent->isFrogsGoMoo; + + if (updatingLimits) { + if (QString::fromStdString(params_memory.get("UpdateSpeedLimitsStatus")) == "Completed!") { + updatingLimits = false; + + updateSpeedLimitsToggle->setValue(tr("Completed!")); + + QTimer::singleShot(2500, [this]() { + updateSpeedLimitsToggle->clearCheckedButtons(); + updateSpeedLimitsToggle->setValue(""); + updateSpeedLimitsToggle->setVisibleButton(0, false); + updateSpeedLimitsToggle->setVisibleButton(1, true); + + params_memory.remove("UpdateSpeedLimitsStatus"); + }); + } else { + updateSpeedLimitsToggle->setValue(QString::fromStdString(params_memory.get("UpdateSpeedLimitsStatus"))); + } + } else { + updateSpeedLimitsToggle->setEnabledButton(1, frogpilot_scene.online && util::system_time_valid() && parked); + updateSpeedLimitsToggle->setValue(frogpilot_scene.online ? (parked ? "" : "Not parked") : tr("Offline...")); + } + + parent->keepScreenOn = primelessLayout->currentIndex() == 1 || updatingLimits; +} + +void FrogPilotNavigationPanel::updateStep() { + QString currentStep; + if (mapboxPublicKeySet) { + currentStep = "../../frogpilot/navigation/navigation_training/public_key_set.png"; + } else { + currentStep = "../../frogpilot/navigation/navigation_training/no_keys_set.png"; + } + + QPixmap pixmap; + pixmap.load(currentStep); + imageLabel->setPixmap(pixmap.scaledToWidth(1500, Qt::SmoothTransformation)); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/navigation_settings.h b/frogpilot/ui/qt/offroad/navigation_settings.h new file mode 100644 index 000000000..39d997184 --- /dev/null +++ b/frogpilot/ui/qt/offroad/navigation_settings.h @@ -0,0 +1,47 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotNavigationPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotNavigationPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void closeSubPanel(); + void openSubPanel(); + +protected: + void hideEvent(QHideEvent *event); + void showEvent(QShowEvent *event) override; + +private: + void mousePressEvent(QMouseEvent *event); + void updateButtons(); + void updateState(const UIState &s, const FrogPilotUIState &fs); + void updateStep(); + + bool forceOpenDescriptions; + bool mapboxPublicKeySet; + bool mapboxSecretKeySet; + bool updatingLimits; + + FrogPilotButtonsControl *publicMapboxKeyControl; + FrogPilotButtonsControl *secretMapboxKeyControl; + ButtonControl *setupButton; + + FrogPilotButtonControl *updateSpeedLimitsToggle; + + FrogPilotSettingsWindow *parent; + + LabelControl *ipLabel; + + Params params; + + QLabel *imageLabel; + + QNetworkAccessManager *networkManager; + + QStackedLayout *primelessLayout; +}; diff --git a/frogpilot/ui/qt/offroad/sounds_settings.cc b/frogpilot/ui/qt/offroad/sounds_settings.cc new file mode 100644 index 000000000..703983932 --- /dev/null +++ b/frogpilot/ui/qt/offroad/sounds_settings.cc @@ -0,0 +1,164 @@ +#include "frogpilot/ui/qt/offroad/sounds_settings.h" + +FrogPilotSoundsPanel::FrogPilotSoundsPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *soundsLayout = new QStackedLayout(); + addItem(soundsLayout); + + FrogPilotListWidget *soundsList = new FrogPilotListWidget(this); + + ScrollView *soundsPanel = new ScrollView(soundsList, this); + + soundsLayout->addWidget(soundsPanel); + + FrogPilotListWidget *alertVolumeControlList = new FrogPilotListWidget(this); + FrogPilotListWidget *customAlertsList = new FrogPilotListWidget(this); + + ScrollView *alertVolumeControlPanel = new ScrollView(alertVolumeControlList, this); + ScrollView *customAlertsPanel = new ScrollView(customAlertsList, this); + + soundsLayout->addWidget(alertVolumeControlPanel); + soundsLayout->addWidget(customAlertsPanel); + + const std::vector> soundsToggles { + {"AlertVolumeControl", tr("Alert Volume Controller"), tr("Set how loud each type of openpilot alert is to keep routine prompts from becoming distracting."), "../../frogpilot/assets/toggle_icons/icon_mute.png"}, + {"DisengageVolume", tr("Disengage Volume"), tr("Set the volume for alerts when openpilot disengages.

Examples include: \"Cruise Fault: Restart the Car\", \"Parking Brake Engaged\", \"Pedal Pressed\"."), ""}, + {"EngageVolume", tr("Engage Volume"), tr("Set the volume for the chime when openpilot engages, such as after pressing the \"RESUME\" or \"SET\" steering wheel buttons."), ""}, + {"PromptVolume", tr("Prompt Volume"), tr("Set the volume for prompts that need attention.

Examples include: \"Car Detected in Blindspot\", \"Steering Temporarily Unavailable\", \"Turn Exceeds Steering Limit\"."), ""}, + {"PromptDistractedVolume", tr("Prompt Distracted Volume"), tr("Set the volume for prompts when openpilot detects driver distraction or unresponsiveness.

Examples include: \"Pay Attention\", \"Touch Steering Wheel\"."), ""}, + {"RefuseVolume", tr("Refuse Volume"), tr("Set the volume for alerts when openpilot refuses to engage.

Examples include: \"Brake Hold Active\", \"Door Open\", \"Seatbelt Unlatched\"."), ""}, + {"WarningSoftVolume", tr("Warning Soft Volume"), tr("Set the volume for softer warnings about potential risks.

Examples include: \"BRAKE! Risk of Collision\", \"Steering Temporarily Unavailable\"."), ""}, + {"WarningImmediateVolume", tr("Warning Immediate Volume"), tr("Set the volume for the loudest warnings that require urgent attention.

Examples include: \"DISENGAGE IMMEDIATELY — Driver Distracted\", \"DISENGAGE IMMEDIATELY — Driver Unresponsive\"."), ""}, + + {"CustomAlerts", tr("FrogPilot Alerts"), tr("Optional FrogPilot alerts that highlight driving events in a more noticeable way."), "../../frogpilot/assets/toggle_icons/icon_green_light.png"}, + {"GoatScream", tr("Goat Scream"), tr("Play the infamous \"Goat Scream\" when the steering controller reaches its limit. Based on the \"Turn Exceeds Steering Limit\" event."), ""}, + {"GreenLightAlert", tr("Green Light Alert"), tr("Play an alert when the model predicts a red light has turned green.

Disclaimer: openpilot does not explicitly detect traffic lights. This alert is based on end-to-end model predictions from camera input and may trigger even when the light has not changed."), ""}, + {"LeadDepartingAlert", tr("Lead Departing Alert"), tr("Play an alert when the lead vehicle departs from a stop."), ""}, + {"LoudBlindspotAlert", tr("Loud \"Car Detected in Blindspot\" Alert"), tr("Play a louder alert if a vehicle is in the blind spot when attempting to change lanes. Based on the \"Car Detected in Blindspot\" event."), ""}, + {"SpeedLimitChangedAlert", tr("Speed Limit Changed Alert"), tr("Play an alert when the posted speed limit changes."), ""} + }; + + for (const auto &[param, title, desc, icon] : soundsToggles) { + AbstractControl *soundsToggle; + + if (param == "AlertVolumeControl") { + FrogPilotManageControl *alertVolumeControlToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(alertVolumeControlToggle, &FrogPilotManageControl::manageButtonClicked, [soundsLayout, alertVolumeControlPanel]() { + soundsLayout->setCurrentWidget(alertVolumeControlPanel); + }); + soundsToggle = alertVolumeControlToggle; + } else if (alertVolumeControlKeys.contains(param)) { + std::map volumeLabels; + for (int i = 0; i <= 101; ++i) { + volumeLabels[i] = i == 0 ? tr("Muted") : i == 101 ? tr("Auto") : QString::number(i) + "%"; + } + std::vector alertButton{tr("Test")}; + if (param == "WarningImmediateVolume" || param == "WarningSoftVolume") { + soundsToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 25, 101, QString(), volumeLabels, 1, true, {}, alertButton, false, false); + } else { + soundsToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 0, 101, QString(), volumeLabels, 1, true, {}, alertButton, false, false); + } + + } else if (param == "CustomAlerts") { + FrogPilotManageControl *customAlertsToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(customAlertsToggle, &FrogPilotManageControl::manageButtonClicked, [soundsLayout, customAlertsPanel]() { + soundsLayout->setCurrentWidget(customAlertsPanel); + }); + soundsToggle = customAlertsToggle; + + } else { + soundsToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = soundsToggle; + + if (alertVolumeControlKeys.contains(param)) { + alertVolumeControlList->addItem(soundsToggle); + } else if (customAlertsKeys.contains(param)) { + customAlertsList->addItem(soundsToggle); + } else { + soundsList->addItem(soundsToggle); + + parentKeys.insert(param); + } + + if (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(soundsToggle)) { + QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + emit openSubPanel(); + openDescriptions(forceOpenDescriptions, toggles); + }); + } + + QObject::connect(soundsToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(soundsToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [soundsLayout, soundsPanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + soundsLayout->setCurrentWidget(soundsPanel); + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotSoundsPanel::updateState); + + for (auto &[key, toggle] : toggles) { + if (alertVolumeControlKeys.contains(key)) { + toggle->setVisible(true); + } + } + + updateToggles(); +} + +void FrogPilotSoundsPanel::showEvent(QShowEvent *event) { + updateToggles(); +} + +void FrogPilotSoundsPanel::updateState(const UIState &s) { + if (!isVisible()) { + return; + } + + started = s.scene.started; +} + +void FrogPilotSoundsPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "LoudBlindspotAlert") { + setVisible &= parent->hasBSM; + } + + else if (key == "SpeedLimitChangedAlert") { + setVisible &= params.getBool("ShowSpeedLimits") || (parent->hasOpenpilotLongitudinal && params.getBool("SpeedLimitController")); + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (alertVolumeControlKeys.contains(key)) { + toggles["AlertVolumeControl"]->setVisible(true); + } else if (customAlertsKeys.contains(key)) { + toggles["CustomAlerts"]->setVisible(true); + } + } + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/sounds_settings.h b/frogpilot/ui/qt/offroad/sounds_settings.h new file mode 100644 index 000000000..6e9d3f7fc --- /dev/null +++ b/frogpilot/ui/qt/offroad/sounds_settings.h @@ -0,0 +1,34 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotSoundsPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotSoundsPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateState(const UIState &s); + void updateToggles(); + + bool forceOpenDescriptions; + bool started; + + std::map toggles; + + QSet alertVolumeControlKeys {"DisengageVolume", "EngageVolume", "PromptDistractedVolume", "PromptVolume", "RefuseVolume", "WarningImmediateVolume", "WarningSoftVolume"}; + QSet customAlertsKeys {"GoatScream", "GreenLightAlert", "LeadDepartingAlert", "LoudBlindspotAlert", "SpeedLimitChangedAlert"}; + + QSet parentKeys; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/theme_settings.cc b/frogpilot/ui/qt/offroad/theme_settings.cc new file mode 100644 index 000000000..5a9d90a9b --- /dev/null +++ b/frogpilot/ui/qt/offroad/theme_settings.cc @@ -0,0 +1,869 @@ +#include "frogpilot/ui/qt/offroad/theme_settings.h" + +bool isUserCreatedTheme(const QString &themeName) { + return themeName.endsWith("-user_created"); +} + +void updateAssetParam(const QString &assetParam, Params ¶ms, const QString &value, bool add) { + QStringList assets = QString::fromStdString(params.get(assetParam.toStdString())).split(",", QString::SkipEmptyParts); + if (add) { + if (!assets.contains(value)) { + assets.append(value); + } + } else { + assets.removeAll(value); + } + assets.sort(); + + params.put(assetParam.toStdString(), assets.join(",").toStdString()); +} + +void deleteThemeAsset(QDir &directory, const QString &subFolder, const QString &assetParam, const QString &themeToDelete, Params ¶ms) { + bool useFiles = subFolder.isEmpty(); + + QString baseName = themeToDelete.toLower(); + baseName.replace("(", "-").replace(")", "").replace(" ", "-"); + baseName.remove(QRegularExpression("[^a-z0-9\\-]")); + while (baseName.endsWith("-")) { + baseName.chop(1); + } + + QString baseUnderscore = baseName; + baseUnderscore.replace("-", "_"); + + QStringList candidateNames = { + baseName, + baseName + "-user-created", + baseUnderscore, + baseUnderscore + "-user_created" + }; + + if (useFiles) { + QStringList files = directory.entryList(QDir::Files); + for (QString &file : files) { + QString normalizedFile = QFileInfo(file).baseName().toLower(); + normalizedFile.replace("_", "-"); + normalizedFile.remove(QRegularExpression("[^a-z0-9\\-~]")); + + if (candidateNames.contains(normalizedFile)) { + QFile::remove(directory.filePath(file)); + break; + } + } + } else { + for (QString &candidate : candidateNames) { + QString fullSubPath = QDir(candidate).filePath(subFolder); + QDir targetDir(directory.filePath(fullSubPath)); + + if (targetDir.exists()) { + targetDir.removeRecursively(); + break; + } + } + } + + updateAssetParam(assetParam, params, themeToDelete, true); +} + +void downloadThemeAsset(const QString &input, const std::string ¶mKey, const QString &assetParam, Params ¶ms, Params ¶ms_memory) { + QString output = input; + int tilde = output.indexOf("~"); + if (tilde >= 0) { + output = output.left(tilde).toLower() + "~" + output.mid(tilde + 1); + } else { + output = output.toLower(); + } + output.remove("(").remove(")"); + output.replace(" ", input.contains("(") ? "-" : "_"); + + params_memory.put(paramKey, output.toStdString()); +} + +QStringList getHolidayThemes() { + return QStringList() + << "New Year's" + << "Valentine's Day" + << "St. Patrick's Day" + << "World Frog Day" + << "April Fools" + << "Easter" + << "May the Fourth" + << "Cinco de Mayo" + << "Stitch Day" + << "Fourth of July" + << "Halloween" + << "Thanksgiving" + << "Christmas"; +} + +QStringList getThemeList(const bool &randomThemes, const QDir &themePacksDirectory, const QString &subFolder, const QString &assetParam, Params ¶ms) { + bool useFiles = subFolder.isEmpty(); + + QString currentAsset = randomThemes ? "" : QString::fromStdString(params.get(assetParam.toStdString())); + + QStringList themeList; + for (const QFileInfo &entry : themePacksDirectory.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot)) { + if (entry.baseName() == currentAsset) { + continue; + } + + if (useFiles && entry.isDir()) { + continue; + } + + if (!useFiles) { + QString targetPath = QDir(entry.filePath()).filePath(subFolder); + if (!QFileInfo(targetPath).exists()) { + continue; + } + } + + QString baseName = entry.baseName(); + bool userCreated = isUserCreatedTheme(baseName); + if (userCreated) { + baseName = baseName.replace("-user_created", ""); + } + + int tildeIdx = baseName.indexOf("~"); + QString creator; + if (tildeIdx >= 0) { + creator = baseName.mid(tildeIdx + 1); + baseName = baseName.left(tildeIdx); + } + + QStringList parts = baseName.split(baseName.contains("-") ? "-" : "_", QString::SkipEmptyParts); + for (QString &part : parts) { + part[0] = part[0].toUpper(); + } + + QString displayName; + if (userCreated) { + displayName = parts.join(" "); + } else { + displayName = (parts.size() <= 1 || useFiles) ? parts.join(" ") : QString("%1 (%2)").arg(parts[0], parts.mid(1).join(" ")); + } + + if (userCreated) { + displayName += " 🌟"; + } + if (!creator.isEmpty()) { + displayName += " - by: " + creator; + } + + themeList.append(displayName); + } + + return themeList; +} + +QString getThemeName(const std::string ¶mKey, Params ¶ms) { + QString value = QString::fromStdString(params.get(paramKey)); + + QString baseName = value; + + int tildeIdx = baseName.indexOf("~"); + QString creator; + if (tildeIdx >= 0) { + creator = baseName.mid(tildeIdx + 1); + baseName = baseName.left(tildeIdx); + } + + QStringList parts = baseName.split(baseName.contains("-") ? "-" : "_", QString::SkipEmptyParts); + for (QString &part : parts) { + part[0] = part[0].toUpper(); + } + + QString displayName; + if (baseName.contains("-") && parts.size() > 1) { + displayName = QString("%1 (%2)").arg(parts[0], parts.mid(1).join(" ")); + } else { + displayName = parts.join(" "); + } + + if (isUserCreatedTheme(value)) { + displayName = displayName.split(" (")[0] + " 🌟"; + } + if (!creator.isEmpty()) { + displayName += " - by: " + creator; + } + + return displayName; +} + +QString storeThemeName(const QString &input, const std::string ¶mKey, Params ¶ms) { + QString output = input.toLower().remove("(").remove(")").remove("'").remove("."); + output.replace(" ", input.contains("(") ? "-" : "_"); + output.replace("_🌟", "-user_created"); + output = output.trimmed(); + + params.put(paramKey, output.toStdString()); + + return getThemeName(paramKey, params); +} + +FrogPilotThemesPanel::FrogPilotThemesPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *themesLayout = new QStackedLayout(); + addItem(themesLayout); + + FrogPilotListWidget *themesList = new FrogPilotListWidget(this); + + ScrollView *themesPanel = new ScrollView(themesList, this); + + themesLayout->addWidget(themesPanel); + + FrogPilotListWidget *customThemesList = new FrogPilotListWidget(this); + + ScrollView *customThemesPanel = new ScrollView(customThemesList, this); + + themesLayout->addWidget(customThemesPanel); + + const std::vector> themeToggles { + {"CustomThemes", tr("Custom Themes"), tr("The overall look and feel of openpilot. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), "../../frogpilot/assets/toggle_icons/icon_frog.png"}, + {"ColorScheme", tr("Color Scheme"), tr("The color scheme used throughout openpilot. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"DistanceIconPack", tr("Distance Button"), tr("The distance button icons shown on the driving screen. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"IconPack", tr("Icon Pack"), tr("The icon style used across openpilot. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"SignalAnimation", tr("Turn Signal"), tr("Themed turn-signal animations. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"SoundPack", tr("Sound Pack"), tr("The sound pack used by openpilot. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"WheelIcon", tr("Steering Wheel"), tr("The steering-wheel icon shown at the top-right of the driving screen. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"DownloadStatusLabel", tr("Download Status"), "", ""}, + + {"HolidayThemes", tr("Holiday Themes"), tr("Themes based on U.S. holidays. Minor holidays last one day; major holidays (Christmas, Easter, Halloween) run for a full week."), "../../frogpilot/assets/toggle_icons/icon_calendar.png"}, + {"RainbowPath", tr("Rainbow Path"), tr("Color the driving path like a Mario Kart–style \"Rainbow Road\"."), "../../frogpilot/assets/toggle_icons/icon_rainbow.png"}, + {"RandomEvents", tr("Random Events"), tr("Occasional on-screen effects triggered by driving conditions. These are purely a visual and don't impact how openpilot drives!"), "../../frogpilot/assets/toggle_icons/icon_random.png"}, + {"RandomThemes", tr("Random Themes"), tr("Pick a random theme between each drive from the themes you have downloaded. Great for variety without changing settings while driving."), "../../frogpilot/assets/toggle_icons/icon_random_themes.png"}, + {"StartupAlert", tr("Startup Alert"), tr("Customize the \"Startup Alert\" message shown at the start of each drive."), "../../frogpilot/assets/toggle_icons/icon_message.png"} + }; + + for (const auto &[param, title, desc, icon] : themeToggles) { + AbstractControl *themeToggle; + + if (param == "CustomThemes") { + FrogPilotManageControl *personalizeOpenpilotToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(personalizeOpenpilotToggle, &FrogPilotManageControl::manageButtonClicked, [customThemesPanel, themesLayout]() { + themesLayout->setCurrentWidget(customThemesPanel); + }); + themeToggle = personalizeOpenpilotToggle; + } else if (param == "ColorScheme") { + manageColorSchemeButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageColorSchemeButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList colorSchemes = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "colors", "ColorScheme", params); + + if (id == 0) { + QString colorSchemeToDelete = MultiOptionDialog::getSelection(tr("Select a color scheme to delete"), colorSchemes, "", this); + if (!colorSchemeToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" color scheme?").arg(colorSchemeToDelete), tr("Delete"), this)) { + colorsDownloaded = false; + + deleteThemeAsset(themePacksDirectory, "colors", "DownloadableColors", colorSchemeToDelete, params); + } + } else if (id == 1) { + if (colorDownloading) { + cancellingDownload = true; + + params_memory.putBool("CancelThemeDownload", true); + + QTimer::singleShot(2500, [this]() { + cancellingDownload = false; + colorDownloading = false; + themeDownloading = false; + + params_memory.putBool("CancelThemeDownload", false); + }); + } else { + QStringList downloadableColorSchemes = QString::fromStdString(params.get("DownloadableColors")).split(","); + colorSchemeToDownload = MultiOptionDialog::getSelection(tr("Select a color scheme to download"), downloadableColorSchemes, "", this); + if (!colorSchemeToDownload.isEmpty()) { + colorDownloading = true; + themeDownloading = true; + + params_memory.put("ThemeDownloadProgress", "Downloading..."); + + downloadThemeAsset(colorSchemeToDownload, "ColorToDownload", "DownloadableColors", params, params_memory); + + downloadStatusLabel->setText(tr("Downloading...")); + } + } + } else if (id == 2) { + colorSchemes.append("Stock"); + colorSchemes.append(getHolidayThemes()); + colorSchemes.sort(); + + QString colorSchemeToSelect = MultiOptionDialog::getSelection(tr("Select a color scheme"), colorSchemes, getThemeName("ColorScheme", params), this); + if (!colorSchemeToSelect.isEmpty()) { + manageColorSchemeButton->setValue(storeThemeName(colorSchemeToSelect, "ColorScheme", params)); + } + } + }); + manageColorSchemeButton->setValue(getThemeName(param.toStdString(), params)); + themeToggle = manageColorSchemeButton; + } else if (param == "DistanceIconPack") { + manageDistanceIconPackButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageDistanceIconPackButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList distanceIconPacks = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "distance_icons", "DistanceIconPack", params); + + if (id == 0) { + QString distanceIconPackToDelete = MultiOptionDialog::getSelection(tr("Select a distance icon pack to delete"), distanceIconPacks, "", this); + if (!distanceIconPackToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" distance icon pack?").arg(distanceIconPackToDelete), tr("Delete"), this)) { + distanceIconsDownloaded = false; + + deleteThemeAsset(themePacksDirectory, "distance_icons", "DownloadableDistanceIcons", distanceIconPackToDelete, params); + } + } else if (id == 1) { + if (distanceIconDownloading) { + cancellingDownload = true; + + params_memory.putBool("CancelThemeDownload", true); + + QTimer::singleShot(2500, [this]() { + cancellingDownload = false; + distanceIconDownloading = false; + themeDownloading = false; + + params_memory.putBool("CancelThemeDownload", false); + }); + } else { + QStringList downloadableDistanceIconPacks = QString::fromStdString(params.get("DownloadableDistanceIcons")).split(","); + distanceIconPackToDownload = MultiOptionDialog::getSelection(tr("Select a distance icon pack to download"), downloadableDistanceIconPacks, "", this); + if (!distanceIconPackToDownload.isEmpty()) { + distanceIconDownloading = true; + themeDownloading = true; + + params_memory.put("ThemeDownloadProgress", "Downloading..."); + + downloadThemeAsset(distanceIconPackToDownload, "DistanceIconToDownload", "DownloadableDistanceIcons", params, params_memory); + + downloadStatusLabel->setText(tr("Downloading...")); + } + } + } else if (id == 2) { + distanceIconPacks.append("Stock"); + distanceIconPacks.append(getHolidayThemes()); + distanceIconPacks.sort(); + + QString distanceIconPackToSelect = MultiOptionDialog::getSelection(tr("Select a distance icon pack"), distanceIconPacks, getThemeName("DistanceIconPack", params), this); + if (!distanceIconPackToSelect.isEmpty()) { + manageDistanceIconPackButton->setValue(storeThemeName(distanceIconPackToSelect, "DistanceIconPack", params)); + } + } + }); + manageDistanceIconPackButton->setValue(getThemeName(param.toStdString(), params)); + themeToggle = manageDistanceIconPackButton; + } else if (param == "IconPack") { + manageIconPackButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageIconPackButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList iconPacks = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "icons", "IconPack", params); + + if (id == 0) { + QString iconPackToDelete = MultiOptionDialog::getSelection(tr("Select an icon pack to delete"), iconPacks, "", this); + if (!iconPackToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" icon pack?").arg(iconPackToDelete), tr("Delete"), this)) { + iconsDownloaded = false; + + deleteThemeAsset(themePacksDirectory, "icons", "DownloadableIcons", iconPackToDelete, params); + } + } else if (id == 1) { + if (iconDownloading) { + cancellingDownload = true; + + params_memory.putBool("CancelThemeDownload", true); + + QTimer::singleShot(2500, [this]() { + cancellingDownload = false; + iconDownloading = false; + themeDownloading = false; + + params_memory.putBool("CancelThemeDownload", false); + }); + } else { + QStringList downloadableIconPacks = QString::fromStdString(params.get("DownloadableIcons")).split(","); + iconPackToDownload = MultiOptionDialog::getSelection(tr("Select an icon pack to download"), downloadableIconPacks, "", this); + if (!iconPackToDownload.isEmpty()) { + iconDownloading = true; + themeDownloading = true; + + params_memory.put("ThemeDownloadProgress", "Downloading..."); + + downloadThemeAsset(iconPackToDownload, "IconToDownload", "DownloadableIcons", params, params_memory); + + downloadStatusLabel->setText(tr("Downloading...")); + } + } + } else if (id == 2) { + iconPacks.append("Stock"); + iconPacks.append(getHolidayThemes()); + iconPacks.sort(); + + QString iconPackToSelect = MultiOptionDialog::getSelection(tr("Select an icon pack"), iconPacks, getThemeName("IconPack", params), this); + if (!iconPackToSelect.isEmpty()) { + manageIconPackButton->setValue(storeThemeName(iconPackToSelect, "IconPack", params)); + } + } + }); + manageIconPackButton->setValue(getThemeName(param.toStdString(), params)); + themeToggle = manageIconPackButton; + } else if (param == "SignalAnimation") { + manageSignalAnimationButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageSignalAnimationButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList signalAnimations = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "signals", "SignalAnimation", params); + + if (id == 0) { + QString signalAnimationToDelete = MultiOptionDialog::getSelection(tr("Select a signal animation to delete"), signalAnimations, "", this); + if (!signalAnimationToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" signal animation?").arg(signalAnimationToDelete), tr("Delete"), this)) { + signalsDownloaded = false; + + deleteThemeAsset(themePacksDirectory, "signals", "DownloadableSignals", signalAnimationToDelete, params); + } + } else if (id == 1) { + if (signalDownloading) { + cancellingDownload = true; + + params_memory.putBool("CancelThemeDownload", true); + + QTimer::singleShot(2500, [this]() { + cancellingDownload = false; + signalDownloading = false; + themeDownloading = false; + + params_memory.putBool("CancelThemeDownload", false); + }); + } else { + QStringList downloadableSignalAnimations = QString::fromStdString(params.get("DownloadableSignals")).split(","); + signalAnimationToDownload = MultiOptionDialog::getSelection(tr("Select a signal animation to download"), downloadableSignalAnimations, "", this); + if (!signalAnimationToDownload.isEmpty()) { + signalDownloading = true; + themeDownloading = true; + + params_memory.put("ThemeDownloadProgress", "Downloading..."); + + downloadThemeAsset(signalAnimationToDownload, "SignalToDownload", "DownloadableSignals", params, params_memory); + + downloadStatusLabel->setText(tr("Downloading...")); + } + } + } else if (id == 2) { + signalAnimations.append("None"); + signalAnimations.append(getHolidayThemes()); + signalAnimations.sort(); + + QString signalAnimationToSelect = MultiOptionDialog::getSelection(tr("Select a signal animation"), signalAnimations, getThemeName("SignalAnimation", params), this); + if (!signalAnimationToSelect.isEmpty()) { + manageSignalAnimationButton->setValue(storeThemeName(signalAnimationToSelect, "SignalAnimation", params)); + } + } + }); + manageSignalAnimationButton->setValue(getThemeName(param.toStdString(), params)); + themeToggle = manageSignalAnimationButton; + } else if (param == "SoundPack") { + manageSoundPackButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageSoundPackButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList soundPacks = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "sounds", "SoundPack", params); + + if (id == 0) { + QString soundPackToDelete = MultiOptionDialog::getSelection(tr("Select a sound pack to delete"), soundPacks, "", this); + if (!soundPackToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" sound pack?").arg(soundPackToDelete), tr("Delete"), this)) { + soundsDownloaded = false; + + deleteThemeAsset(themePacksDirectory, "sounds", "DownloadableSounds", soundPackToDelete, params); + } + } else if (id == 1) { + if (soundDownloading) { + cancellingDownload = true; + + params_memory.putBool("CancelThemeDownload", true); + + QTimer::singleShot(2500, [this]() { + cancellingDownload = false; + soundDownloading = false; + themeDownloading = false; + + params_memory.putBool("CancelThemeDownload", false); + }); + } else { + QStringList downloadableSoundPacks = QString::fromStdString(params.get("DownloadableSounds")).split(","); + soundPackToDownload = MultiOptionDialog::getSelection(tr("Select a sound pack to download"), downloadableSoundPacks, "", this); + if (!soundPackToDownload.isEmpty()) { + soundDownloading = true; + themeDownloading = true; + + params_memory.put("ThemeDownloadProgress", "Downloading..."); + + downloadThemeAsset(soundPackToDownload, "SoundToDownload", "DownloadableSounds", params, params_memory); + + downloadStatusLabel->setText(tr("Downloading...")); + } + } + } else if (id == 2) { + soundPacks.append("Stock"); + soundPacks.append(getHolidayThemes()); + soundPacks.sort(); + + QString soundPackToSelect = MultiOptionDialog::getSelection(tr("Select a sound pack"), soundPacks, getThemeName("SoundPack", params), this); + if (!soundPackToSelect.isEmpty()) { + manageSoundPackButton->setValue(storeThemeName(soundPackToSelect, "SoundPack", params)); + } + } + }); + manageSoundPackButton->setValue(getThemeName(param.toStdString(), params)); + themeToggle = manageSoundPackButton; + } else if (param == "WheelIcon") { + manageWheelIconsButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageWheelIconsButton, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + QStringList wheelIcons = getThemeList(randomThemes, QDir(wheelsDirectory.path()), "", "WheelIcon", params); + + if (id == 0) { + QString wheelIconToDelete = MultiOptionDialog::getSelection(tr("Select a steering wheel to delete"), wheelIcons, "", this); + if (!wheelIconToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" steering wheel?").arg(wheelIconToDelete), tr("Delete"), this)) { + wheelsDownloaded = false; + + deleteThemeAsset(wheelsDirectory, "", "DownloadableWheels", wheelIconToDelete, params); + } + } else if (id == 1) { + if (wheelDownloading) { + cancellingDownload = true; + + params_memory.putBool("CancelThemeDownload", true); + + QTimer::singleShot(2500, [this]() { + cancellingDownload = false; + wheelDownloading = false; + themeDownloading = false; + + params_memory.putBool("CancelThemeDownload", false); + }); + } else { + QStringList downloadableWheels = QString::fromStdString(params.get("DownloadableWheels")).split(","); + wheelToDownload = MultiOptionDialog::getSelection(tr("Select a steering wheel to download"), downloadableWheels, "", this); + if (!wheelToDownload.isEmpty()) { + wheelDownloading = true; + themeDownloading = true; + + params_memory.put("ThemeDownloadProgress", "Downloading..."); + + downloadThemeAsset(wheelToDownload, "WheelToDownload", "DownloadableWheels", params, params_memory); + + downloadStatusLabel->setText(tr("Downloading...")); + } + } + } else if (id == 2) { + wheelIcons.append("None"); + wheelIcons.append("Stock"); + wheelIcons.append(getHolidayThemes()); + wheelIcons.sort(); + + QString steeringWheelToSelect = MultiOptionDialog::getSelection(tr("Select a steering wheel"), wheelIcons, getThemeName("WheelIcon", params), this); + if (!steeringWheelToSelect.isEmpty()) { + manageWheelIconsButton->setValue(storeThemeName(steeringWheelToSelect, "WheelIcon", params)); + } + } + }); + manageWheelIconsButton->setValue(getThemeName(param.toStdString(), params)); + themeToggle = manageWheelIconsButton; + } else if (param == "DownloadStatusLabel") { + downloadStatusLabel = new LabelControl(title, tr("Idle")); + themeToggle = downloadStatusLabel; + + } else if (param == "RandomThemes") { + std::vector randomThemesToggles{"RandomThemesHolidays"}; + std::vector randomThemesToggleNames{tr("Include Holiday Themes")}; + themeToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, randomThemesToggles, randomThemesToggleNames); + + } else if (param == "StartupAlert") { + FrogPilotButtonsControl *startupAlertButton = new FrogPilotButtonsControl(title, desc, icon, {tr("STOCK"), tr("FROGPILOT"), tr("CUSTOM"), tr("CLEAR")}, true); + + QString currentTop = QString::fromStdString(params.get("StartupMessageTop")); + QString currentBottom = QString::fromStdString(params.get("StartupMessageBottom")); + + QString stockTop = "Be ready to take over at any time"; + QString stockBottom = "Always keep hands on wheel and eyes on road"; + + QString frogpilotTop = "Hop in and buckle up!"; + QString frogpilotBottom = "Human-tested, frog-approved 🐸"; + + if (currentTop == stockTop && currentBottom == stockBottom) { + startupAlertButton->setCheckedButton(0); + } else if (currentTop == frogpilotTop && currentBottom == frogpilotBottom) { + startupAlertButton->setCheckedButton(1); + } else if (!currentTop.isEmpty() || !currentBottom.isEmpty()) { + startupAlertButton->setCheckedButton(2); + } + + QObject::connect(startupAlertButton, &FrogPilotButtonsControl::buttonClicked, [=](int id) { + int maxLengthTop = 35; + int maxLengthBottom = 45; + + if (id == 0) { + params.put("StartupMessageTop", stockTop.toStdString()); + params.put("StartupMessageBottom", stockBottom.toStdString()); + } else if (id == 1) { + params.put("StartupMessageTop", frogpilotTop.toStdString()); + params.put("StartupMessageBottom", frogpilotBottom.toStdString()); + } else if (id == 2) { + QString currentTop = QString::fromStdString(params.get("StartupMessageTop")); + QString newTop = InputDialog::getText(tr("Enter the text for the top half"), this, tr("Characters: 0/%1").arg(maxLengthTop), false, 1, currentTop, maxLengthTop).trimmed(); + if (!newTop.isEmpty()) { + params.put("StartupMessageTop", newTop.toStdString()); + + QString currentBottom = QString::fromStdString(params.get("StartupMessageBottom")); + QString newBottom = InputDialog::getText(tr("Enter the text for the bottom half"), this, tr("Characters: 0/%1").arg(maxLengthBottom), false, 1, currentBottom, maxLengthBottom).trimmed(); + if (!newBottom.isEmpty()) { + params.put("StartupMessageBottom", newBottom.toStdString()); + } + } + } else if (id == 3) { + if (FrogPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your startup message?"), this)) { + params.remove("StartupMessageTop"); + params.remove("StartupMessageBottom"); + + startupAlertButton->clearCheckedButtons(); + } + } + }); + themeToggle = startupAlertButton; + + } else { + themeToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = themeToggle; + + if (customThemeKeys.contains(param)) { + customThemesList->addItem(themeToggle); + } else { + themesList->addItem(themeToggle); + + if (param == "CustomThemes") { + parentKeys.insert(param); + } + } + + if (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(themeToggle)) { + QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + emit openSubPanel(); + openDescriptions(forceOpenDescriptions, toggles); + }); + } + + QObject::connect(themeToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(themeToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + openDescriptions(forceOpenDescriptions, toggles); + + QObject::connect(static_cast(toggles["CustomThemes"]), &ToggleControl::toggleFlipped, this, &FrogPilotThemesPanel::updateToggles); + QObject::connect(static_cast(toggles["RandomThemes"]), &ToggleControl::toggleFlipped, [this](bool state) { + if (state) { + ConfirmationDialog::alert(tr("\"Random Themes\" only works with downloaded themes, so make sure you download the themes you want it to use!"), this); + + manageColorSchemeButton->setValue(""); + manageColorSchemeButton->setVisibleButton(2, false); + + manageDistanceIconPackButton->setValue(""); + manageDistanceIconPackButton->setVisibleButton(2, false); + + manageIconPackButton->setValue(""); + manageIconPackButton->setVisibleButton(2, false); + + manageSignalAnimationButton->setValue(""); + manageSignalAnimationButton->setVisibleButton(2, false); + + manageSoundPackButton->setValue(""); + manageSoundPackButton->setVisibleButton(2, false); + + manageWheelIconsButton->setValue(""); + manageWheelIconsButton->setVisibleButton(2, false); + } else { + manageColorSchemeButton->setValue(getThemeName("ColorScheme", params)); + manageColorSchemeButton->setVisibleButton(2, true); + + manageDistanceIconPackButton->setValue(getThemeName("DistanceIconPack", params)); + manageDistanceIconPackButton->setVisibleButton(2, true); + + manageIconPackButton->setValue(getThemeName("IconPack", params)); + manageIconPackButton->setVisibleButton(2, true); + + manageSignalAnimationButton->setValue(getThemeName("SignalAnimation", params)); + manageSignalAnimationButton->setVisibleButton(2, true); + + manageSoundPackButton->setValue(getThemeName("SoundPack", params)); + manageSoundPackButton->setVisibleButton(2, true); + + manageWheelIconsButton->setValue(getThemeName("WheelIcon", params)); + manageWheelIconsButton->setVisibleButton(2, true); + } + + randomThemes = state; + }); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [themesLayout, themesPanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + themesLayout->setCurrentWidget(themesPanel); + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotThemesPanel::updateState); +} + +void FrogPilotThemesPanel::showEvent(QShowEvent *event) { + colorsDownloaded = params.get("DownloadableColors").empty(); + distanceIconsDownloaded = params.get("DownloadableDistanceIcons").empty(); + iconsDownloaded = params.get("DownloadableIcons").empty(); + signalsDownloaded = params.get("DownloadableSignals").empty(); + soundsDownloaded = params.get("DownloadableSounds").empty(); + wheelsDownloaded = params.get("DownloadableWheels").empty(); + + if (params.getBool("RandomThemes")) { + manageColorSchemeButton->setValue(""); + manageColorSchemeButton->setVisibleButton(2, false); + + manageDistanceIconPackButton->setValue(""); + manageDistanceIconPackButton->setVisibleButton(2, false); + + manageIconPackButton->setValue(""); + manageIconPackButton->setVisibleButton(2, false); + + manageSignalAnimationButton->setValue(""); + manageSignalAnimationButton->setVisibleButton(2, false); + + manageSoundPackButton->setValue(""); + manageSoundPackButton->setVisibleButton(2, false); + + manageWheelIconsButton->setValue(""); + manageWheelIconsButton->setVisibleButton(2, false); + + randomThemes = true; + } + + updateToggles(); +} + +void FrogPilotThemesPanel::updateState(const UIState &s, const FrogPilotUIState &fs) { + if (!isVisible() || finalizingDownload) { + return; + } + + const FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; + + if (themeDownloading) { + QString progress = QString::fromStdString(params_memory.get("ThemeDownloadProgress")); + bool downloadFailed = progress.contains(QRegularExpression("cancelled|exists|failed|offline", QRegularExpression::CaseInsensitiveOption)); + + if (progress != "Downloading...") { + static const QMap progressTranslations = { + {"Download cancelled...", tr("Download cancelled...")}, + {"Download failed...", tr("Download failed...")}, + {"Downloaded!", tr("Downloaded!")}, + {"GitHub and GitLab are offline...", tr("GitHub and GitLab are offline...")}, + {"Repository unavailable", tr("Repository unavailable")}, + {"Unpacking theme...", tr("Unpacking theme...")}, + {"Verifying authenticity...", tr("Verifying authenticity...")} + }; + + if (progressTranslations.contains(progress)) { + downloadStatusLabel->setText(progressTranslations[progress]); + } else if (progress.endsWith("%")) { + downloadStatusLabel->setText(progress); + } else { + downloadStatusLabel->setText(tr("Idle")); + } + } + + if (progress == "Downloaded!" || downloadFailed) { + finalizingDownload = true; + + QTimer::singleShot(2500, [this]() { + colorDownloading = false; + distanceIconDownloading = false; + finalizingDownload = false; + iconDownloading = false; + signalDownloading = false; + soundDownloading = false; + themeDownloading = false; + wheelDownloading = false; + + colorsDownloaded = params.get("DownloadableColors").empty(); + distanceIconsDownloaded = params.get("DownloadableDistanceIcons").empty(); + iconsDownloaded = params.get("DownloadableIcons").empty(); + signalsDownloaded = params.get("DownloadableSignals").empty(); + soundsDownloaded = params.get("DownloadableSounds").empty(); + wheelsDownloaded = params.get("DownloadableWheels").empty(); + + params_memory.remove("CancelThemeDownload"); + params_memory.remove("ThemeDownloadProgress"); + + downloadStatusLabel->setText(tr("Idle")); + }); + } + } + + bool parked = !s.scene.started || frogpilot_scene.parked || parent->isFrogsGoMoo; + + manageColorSchemeButton->setText(1, colorDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + manageColorSchemeButton->setEnabledButtons(0, !themeDownloading); + manageColorSchemeButton->setEnabledButtons(1, frogpilot_scene.online && (!themeDownloading || colorDownloading) && !cancellingDownload && !finalizingDownload && !colorsDownloaded && parked); + manageColorSchemeButton->setEnabledButtons(2, !themeDownloading); + + manageDistanceIconPackButton->setText(1, distanceIconDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + manageDistanceIconPackButton->setEnabledButtons(0, !themeDownloading); + manageDistanceIconPackButton->setEnabledButtons(1, frogpilot_scene.online && (!themeDownloading || distanceIconDownloading) && !cancellingDownload && !finalizingDownload && !distanceIconsDownloaded && parked); + manageDistanceIconPackButton->setEnabledButtons(2, !themeDownloading); + + manageIconPackButton->setText(1, iconDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + manageIconPackButton->setEnabledButtons(0, !themeDownloading); + manageIconPackButton->setEnabledButtons(1, frogpilot_scene.online && (!themeDownloading || iconDownloading) && !cancellingDownload && !finalizingDownload && !iconsDownloaded && parked); + manageIconPackButton->setEnabledButtons(2, !themeDownloading); + + manageSignalAnimationButton->setText(1, signalDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + manageSignalAnimationButton->setEnabledButtons(0, !themeDownloading); + manageSignalAnimationButton->setEnabledButtons(1, frogpilot_scene.online && (!themeDownloading || signalDownloading) && !cancellingDownload && !finalizingDownload && !signalsDownloaded && parked); + manageSignalAnimationButton->setEnabledButtons(2, !themeDownloading); + + manageSoundPackButton->setText(1, soundDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + manageSoundPackButton->setEnabledButtons(0, !themeDownloading); + manageSoundPackButton->setEnabledButtons(1, frogpilot_scene.online && (!themeDownloading || soundDownloading) && !cancellingDownload && !finalizingDownload && !soundsDownloaded && parked); + manageSoundPackButton->setEnabledButtons(2, !themeDownloading); + + manageWheelIconsButton->setText(1, wheelDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + manageWheelIconsButton->setEnabledButtons(0, !themeDownloading); + manageWheelIconsButton->setEnabledButtons(1, frogpilot_scene.online && (!themeDownloading || wheelDownloading) && !cancellingDownload && !finalizingDownload && !wheelsDownloaded && parked); + manageWheelIconsButton->setEnabledButtons(2, !themeDownloading); + + parent->keepScreenOn = themeDownloading; +} + +void FrogPilotThemesPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "DistanceIconPack") { + setVisible &= params.getBool("QOLVisuals") && params.getBool("OnroadDistanceButton"); + } + + else if (key == "RandomThemes") { + setVisible &= params.getBool("CustomThemes"); + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (customThemeKeys.contains(key)) { + toggles["CustomThemes"]->setVisible(true); + } + } + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/theme_settings.h b/frogpilot/ui/qt/offroad/theme_settings.h new file mode 100644 index 000000000..f146d100f --- /dev/null +++ b/frogpilot/ui/qt/offroad/theme_settings.h @@ -0,0 +1,67 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotThemesPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotThemesPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +protected: + void showEvent(QShowEvent *event) override; + +signals: + void openSubPanel(); + +private: + void updateState(const UIState &s, const FrogPilotUIState &fs); + void updateToggles(); + + bool cancellingDownload; + bool colorDownloading; + bool colorsDownloaded; + bool distanceIconDownloading; + bool distanceIconsDownloaded; + bool finalizingDownload; + bool forceOpenDescriptions; + bool iconDownloading; + bool iconsDownloaded; + bool randomThemes; + bool signalDownloading; + bool signalsDownloaded; + bool soundDownloading; + bool soundsDownloaded; + bool themeDownloading; + bool wheelDownloading; + bool wheelsDownloaded; + + std::map toggles; + + QSet customThemeKeys = {"ColorScheme", "DistanceIconPack", "DownloadStatusLabel", "IconPack", "SignalAnimation", "SoundPack", "WheelIcon"}; + + QSet parentKeys; + + FrogPilotButtonsControl *manageColorSchemeButton; + FrogPilotButtonsControl *manageDistanceIconPackButton; + FrogPilotButtonsControl *manageIconPackButton; + FrogPilotButtonsControl *manageSignalAnimationButton; + FrogPilotButtonsControl *manageSoundPackButton; + FrogPilotButtonsControl *manageWheelIconsButton; + + FrogPilotSettingsWindow *parent; + + LabelControl *downloadStatusLabel; + + QDir themePacksDirectory{"/data/themes/theme_packs/"}; + QDir wheelsDirectory{"/data/themes/steering_wheels/"}; + + QString colorSchemeToDownload; + QString distanceIconPackToDownload; + QString iconPackToDownload; + QString signalAnimationToDownload; + QString soundPackToDownload; + QString wheelToDownload; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/utilities.cc b/frogpilot/ui/qt/offroad/utilities.cc new file mode 100644 index 000000000..80ffdff49 --- /dev/null +++ b/frogpilot/ui/qt/offroad/utilities.cc @@ -0,0 +1,5 @@ +#include "frogpilot/ui/qt/offroad/utilities.h" + +FrogPilotUtilitiesPanel::FrogPilotUtilitiesPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; +} diff --git a/frogpilot/ui/qt/offroad/utilities.h b/frogpilot/ui/qt/offroad/utilities.h new file mode 100644 index 000000000..da0a50c9c --- /dev/null +++ b/frogpilot/ui/qt/offroad/utilities.h @@ -0,0 +1,17 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotUtilitiesPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotUtilitiesPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +private: + bool forceOpenDescriptions; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/vehicle_settings.cc b/frogpilot/ui/qt/offroad/vehicle_settings.cc new file mode 100644 index 000000000..7f78b2667 --- /dev/null +++ b/frogpilot/ui/qt/offroad/vehicle_settings.cc @@ -0,0 +1,281 @@ +#include "frogpilot/ui/qt/offroad/vehicle_settings.h" + +FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *vehiclesLayout = new QStackedLayout(); + addItem(vehiclesLayout); + + FrogPilotListWidget *settingsList = new FrogPilotListWidget(this); + + ScrollView *vehiclesPanel = new ScrollView(settingsList, this); + + vehiclesLayout->addWidget(vehiclesPanel); + + FrogPilotListWidget *gmList = new FrogPilotListWidget(this); + FrogPilotListWidget *hkgList = new FrogPilotListWidget(this); + FrogPilotListWidget *subaruList = new FrogPilotListWidget(this); + FrogPilotListWidget *toyotaList = new FrogPilotListWidget(this); + FrogPilotListWidget *vehicleInfoList = new FrogPilotListWidget(this); + + ScrollView *gmPanel = new ScrollView(gmList, this); + ScrollView *hkgPanel = new ScrollView(hkgList, this); + ScrollView *subaruPanel = new ScrollView(subaruList, this); + ScrollView *toyotaPanel = new ScrollView(toyotaList, this); + ScrollView *vehicleInfoPanel = new ScrollView(vehicleInfoList, this); + + vehiclesLayout->addWidget(gmPanel); + vehiclesLayout->addWidget(hkgPanel); + vehiclesLayout->addWidget(subaruPanel); + vehiclesLayout->addWidget(toyotaPanel); + vehiclesLayout->addWidget(vehicleInfoPanel); + + std::vector> vehicleToggles { + {"GMToggles", tr("General Motors Settings"), tr("FrogPilot features for General Motors vehicles."), ""}, + {"VoltSNG", tr("Stop-and-Go Hack"), tr("Force stop-and-go on the 2017 Chevy Volt."), ""}, + + {"HKGToggles", tr("Hyundai/Kia/Genesis Settings"), tr("FrogPilot features for Genesis, Hyundai, and Kia vehicles."), ""}, + {"TacoTuneHacks", tr("\"Taco Bell Run\" Torque Hack"), tr("The steering torque hack from comma's 2022 \"Taco Bell Run\". Designed to increase steering torque at low speeds for left and right turns."), ""}, + + {"SubaruToggles", tr("Subaru Settings"), tr("FrogPilot features for Subaru vehicles."), ""}, + {"SubaruSNG", tr("Stop and Go"), tr("Stop and go for supported Subaru vehicles."), ""}, + + {"ToyotaToggles", tr("Toyota/Lexus Settings"), tr("FrogPilot features for Lexus and Toyota vehicles."), ""}, + {"ToyotaDoors", tr("Automatically Lock/Unlock Doors"), tr("Automatically lock/unlock doors when shifting in and out of drive."), ""}, + {"ClusterOffset", tr("Dashboard Speed Offset"), tr("The speed offset openpilot uses to match the speed on the dashboard display."), ""}, + {"FrogsGoMoosTweak", tr("FrogsGoMoo's Personal Tweaks"), tr("Personal tweaks by FrogsGoMoo for quicker acceleration and smoother braking."), ""}, + {"LockDoorsTimer", tr("Lock Doors On Ignition Off After"), tr("Automatically lock the doors on ignition off when no one is detected in the front seats."), ""}, + {"SNGHack", tr("Stop-and-Go Hack"), tr("Force stop-and-go on Lexus/Toyota vehicles without stock stop-and-go functionality."), ""}, + + {"VehicleInfo", tr("Vehicle Info"), tr("Information about your vehicle in regards to openpilot support and functionality."), ""}, + {"HardwareDetected", tr("3rd Party Hardware Detected"), tr("Detected 3rd party hardware."), ""}, + {"BlindSpotSupport", tr("Blind Spot Support"), tr("Does openpilot use the vehicle's blind spot data?"), ""}, + {"PedalSupport", tr("comma Pedal Support"), tr("Does your vehicle support the \"comma pedal\"?"), ""}, + {"OpenpilotLongitudinal", tr("openpilot Longitudinal Support"), tr("Can openpilot control the vehicle's acceleration and braking?"), ""}, + {"RadarSupport", tr("Radar Support"), tr("Does openpilot use the vehicle's radar data alongside the device's camera for tracking lead vehicles?"), ""}, + {"SDSUSupport", tr("SDSU Support"), tr("Does your vehicle support \"SDSUs\"?"), ""}, + {"SNGSupport", tr("Stop-and-Go Support"), tr("Does your vehicle support stop-and-go driving?"), ""} + }; + + for (const auto &[param, title, desc, icon] : vehicleToggles) { + AbstractControl *vehicleToggle; + + if (param == "GMToggles") { + ButtonControl *gmButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(gmButton, &ButtonControl::clicked, [vehiclesLayout, gmPanel, this]() { + openDescriptions(forceOpenDescriptions, toggles); + vehiclesLayout->setCurrentWidget(gmPanel); + }); + vehicleToggle = gmButton; + + } else if (param == "HKGToggles") { + ButtonControl *hkgButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(hkgButton, &ButtonControl::clicked, [vehiclesLayout, hkgPanel, this]() { + openDescriptions(forceOpenDescriptions, toggles); + vehiclesLayout->setCurrentWidget(hkgPanel); + }); + vehicleToggle = hkgButton; + + } else if (param == "SubaruToggles") { + ButtonControl *subaruButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(subaruButton, &ButtonControl::clicked, [vehiclesLayout, subaruPanel, this]() { + openDescriptions(forceOpenDescriptions, toggles); + vehiclesLayout->setCurrentWidget(subaruPanel); + }); + vehicleToggle = subaruButton; + + } else if (param == "ToyotaToggles") { + ButtonControl *toyotaButton = new ButtonControl(title, tr("MANAGE"), desc); + QObject::connect(toyotaButton, &ButtonControl::clicked, [vehiclesLayout, toyotaPanel, this]() { + openDescriptions(forceOpenDescriptions, toggles); + vehiclesLayout->setCurrentWidget(toyotaPanel); + }); + vehicleToggle = toyotaButton; + } else if (param == "ToyotaDoors") { + std::vector lockToggles{"LockDoors", "UnlockDoors"}; + std::vector lockToggleNames{tr("Lock"), tr("Unlock")}; + vehicleToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, lockToggles, lockToggleNames); + } else if (param == "LockDoorsTimer") { + std::map autoLockLabels; + for (int i = 0; i <= 300; ++i) { + autoLockLabels[i] = i == 0 ? tr("Never") : QString::number(i) + tr(" seconds"); + } + vehicleToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 300, QString(), autoLockLabels, 5); + } else if (param == "ClusterOffset") { + std::vector clusterOffsetButton{"Reset"}; + FrogPilotParamValueButtonControl *clusterOffsetToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 1.000, 1.050, "x", std::map(), 0.001, false, {}, clusterOffsetButton, false, false); + QObject::connect(clusterOffsetToggle, &FrogPilotParamValueButtonControl::buttonClicked, [clusterOffsetToggle, this]() { + params.putFloat("ClusterOffset", std::stof(params.getKeyDefaultValue("ClusterOffset").value())); + clusterOffsetToggle->refresh(); + }); + vehicleToggle = clusterOffsetToggle; + + } else if (param == "VehicleInfo") { + ButtonControl *VehicleInfoButton = new ButtonControl(title, tr("VIEW"), desc); + QObject::connect(VehicleInfoButton, &ButtonControl::clicked, [vehiclesLayout, vehicleInfoPanel, this]() { + openDescriptions(forceOpenDescriptions, toggles); + vehiclesLayout->setCurrentWidget(vehicleInfoPanel); + }); + vehicleToggle = VehicleInfoButton; + } else if (vehicleInfoKeys.contains(param)) { + vehicleToggle = new LabelControl(title, "", desc); + + } else { + vehicleToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = vehicleToggle; + + if (gmKeys.contains(param)) { + gmList->addItem(vehicleToggle); + } else if (hkgKeys.contains(param)) { + hkgList->addItem(vehicleToggle); + } else if (subaruKeys.contains(param)) { + subaruList->addItem(vehicleToggle); + } else if (toyotaKeys.contains(param)) { + toyotaList->addItem(vehicleToggle); + } else if (vehicleInfoKeys.contains(param)) { + vehicleInfoList->addItem(vehicleToggle); + } else { + settingsList->addItem(vehicleToggle); + + parentKeys.insert(param); + } + + if (ButtonControl *buttonControl = qobject_cast(vehicleToggle)) { + QObject::connect(buttonControl, &ButtonControl::clicked, this, &FrogPilotVehiclesPanel::openSubPanel); + } + + QObject::connect(vehicleToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(vehicleToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + static_cast(toggles["LockDoorsTimer"])->setWarning("Warning: openpilot can't detect if keys are still inside the car, so ensure you have a spare key to prevent accidental lockouts!"); + + QSet rebootKeys = {"TacoTuneHacks"}; + for (const QString &key : rebootKeys) { + QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, [key, this](bool state) { + if (started) { + if (key == "TacoTuneHacks" && state) { + if (FrogPilotConfirmationDialog::toggleReboot(this)) { + Hardware::reboot(); + } + } else if (key != "TacoTuneHacks") { + if (FrogPilotConfirmationDialog::toggleReboot(this)) { + Hardware::reboot(); + } + } + } + }); + } + + openDescriptions(forceOpenDescriptions, toggles); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [vehiclesLayout, vehiclesPanel, this] { + if (forceOpenDescriptions) { + openDescriptions(forceOpenDescriptions, toggles); + } + vehiclesLayout->setCurrentWidget(vehiclesPanel); + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotVehiclesPanel::updateState); +} + +void FrogPilotVehiclesPanel::showEvent(QShowEvent *event) { + if (forceOpenDescriptions) { + } + + QStringList detected; + if (parent->hasPedal) detected << "comma Pedal"; + if (parent->hasSDSU) detected << "SDSU"; + if (parent->hasZSS) detected << "ZSS"; + static_cast(toggles["HardwareDetected"])->setText(detected.isEmpty() ? tr("None") : detected.join(", ")); + + static_cast(toggles["BlindSpotSupport"])->setText(parent->hasBSM ? tr("Yes") : tr("No")); + static_cast(toggles["OpenpilotLongitudinal"])->setText(parent->hasOpenpilotLongitudinal ? tr("Yes") : tr("No")); + static_cast(toggles["PedalSupport"])->setText(parent->canUsePedal ? tr("Yes") : tr("No")); + static_cast(toggles["RadarSupport"])->setText(parent->hasRadar ? tr("Yes") : tr("No")); + static_cast(toggles["SDSUSupport"])->setText(parent->canUseSDSU ? tr("Yes") : tr("No")); + static_cast(toggles["SNGSupport"])->setText(parent->hasSNG ? tr("Yes") : tr("No")); + + updateToggles(); +} + +void FrogPilotVehiclesPanel::updateState(const UIState &s) { + if (!isVisible()) { + return; + } + + started = s.scene.started; +} + +void FrogPilotVehiclesPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (gmKeys.contains(key)) { + setVisible &= parent->isGM; + } else if (hkgKeys.contains(key)) { + setVisible &= parent->isHKG; + } else if (subaruKeys.contains(key)) { + setVisible &= parent->isSubaru; + } else if (toyotaKeys.contains(key)) { + setVisible &= parent->isToyota; + } else if (vehicleInfoKeys.contains(key)) { + setVisible = true; + } + + if (longitudinalKeys.contains(key)) { + setVisible &= parent->hasOpenpilotLongitudinal; + } + + if (key == "SNGHack") { + setVisible &= !parent->hasSNG; + } + + else if (key == "SubaruSNG") { + setVisible &= parent->hasSNG; + } + + else if (key == "TacoTuneHacks") { + setVisible &= parent->isHKGCanFd; + } + + else if (key == "VoltSNG") { + setVisible &= parent->isVolt && !parent->hasSNG; + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (gmKeys.contains(key)) { + toggles["GMToggles"]->setVisible(true); + } else if (hkgKeys.contains(key)) { + toggles["HKGToggles"]->setVisible(true); + } else if (subaruKeys.contains(key)) { + toggles["SubaruToggles"]->setVisible(true); + } else if (toyotaKeys.contains(key)) { + toggles["ToyotaToggles"]->setVisible(true); + } else if (vehicleInfoKeys.contains(key)) { + toggles["VehicleInfo"]->setVisible(true); + } + } + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/vehicle_settings.h b/frogpilot/ui/qt/offroad/vehicle_settings.h new file mode 100644 index 000000000..4716593d7 --- /dev/null +++ b/frogpilot/ui/qt/offroad/vehicle_settings.h @@ -0,0 +1,38 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotVehiclesPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateState(const UIState &s); + void updateToggles(); + + bool forceOpenDescriptions; + bool started; + + std::map toggles; + + QSet gmKeys = {"VoltSNG"}; + QSet hkgKeys = {"TacoTuneHacks"}; + QSet longitudinalKeys = {"FrogsGoMoosTweak", "SNGHack", "VoltSNG"}; + QSet subaruKeys = {"SubaruSNG"}; + QSet toyotaKeys = {"ClusterOffset", "FrogsGoMoosTweak", "LockDoorsTimer", "SNGHack", "ToyotaDoors"}; + QSet vehicleInfoKeys = {"BlindSpotSupport", "HardwareDetected", "OpenpilotLongitudinal", "PedalSupport", "RadarSupport", "SDSUSupport", "SNGSupport"}; + + QSet parentKeys; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/visual_settings.cc b/frogpilot/ui/qt/offroad/visual_settings.cc new file mode 100644 index 000000000..9b4905ffa --- /dev/null +++ b/frogpilot/ui/qt/offroad/visual_settings.cc @@ -0,0 +1,328 @@ +#include "frogpilot/ui/qt/offroad/visual_settings.h" + +FrogPilotVisualsPanel::FrogPilotVisualsPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *visualsLayout = new QStackedLayout(); + addItem(visualsLayout); + + FrogPilotListWidget *visualsList = new FrogPilotListWidget(this); + + ScrollView *visualsPanel = new ScrollView(visualsList, this); + + visualsLayout->addWidget(visualsPanel); + + FrogPilotListWidget *advancedCustomList = new FrogPilotListWidget(this); + FrogPilotListWidget *customUIList = new FrogPilotListWidget(this); + FrogPilotListWidget *modelUIList = new FrogPilotListWidget(this); + FrogPilotListWidget *navigationUIList = new FrogPilotListWidget(this); + FrogPilotListWidget *qualityOfLifeList = new FrogPilotListWidget(this); + + ScrollView *advancedCustomPanel = new ScrollView(advancedCustomList, this); + ScrollView *customUIPanel = new ScrollView(customUIList, this); + ScrollView *modelUIPanel = new ScrollView(modelUIList, this); + ScrollView *navigationUIPanel = new ScrollView(navigationUIList, this); + ScrollView *qualityOfLifePanel = new ScrollView(qualityOfLifeList, this); + + visualsLayout->addWidget(advancedCustomPanel); + visualsLayout->addWidget(customUIPanel); + visualsLayout->addWidget(modelUIPanel); + visualsLayout->addWidget(navigationUIPanel); + visualsLayout->addWidget(qualityOfLifePanel); + + const std::vector> visualToggles { + {"AdvancedCustomUI", tr("Advanced UI Controls"), tr("Advanced visual changes to fine-tune how the driving screen looks."), "../../frogpilot/assets/toggle_icons/icon_advanced_device.png"}, + {"HideSpeed", tr("Hide Current Speed"), tr("Hide the current speed from the driving screen."), ""}, + {"HideLeadMarker", tr("Hide Lead Marker"), tr("Hide the lead-vehicle marker from the driving screen."), ""}, + {"HideMaxSpeed", tr("Hide Max Speed"), tr("Hide the max speed from the driving screen."), ""}, + {"HideAlerts", tr("Hide Non-Critical Alerts"), tr("Hide non-critical alerts from the driving screen."), ""}, + {"HideSpeedLimit", tr("Hide Speed Limits"), tr("Hide posted speed limits from the driving screen."), ""}, + {"WheelSpeed", tr("Use Wheel Speed"), tr("Use the vehicle's wheel speed instead of the cluster speed. This is purely a visual change and doesn't impact how openpilot drives!"), ""}, + + {"CustomUI", tr("Driving Screen Widgets"), tr("Custom FrogPilot widgets for the driving screen."), "../assets/icons/calibration.png"}, + {"AccelerationPath", tr("Acceleration Path"), tr("Color the driving path by planned acceleration and braking."), ""}, + {"AdjacentPath", tr("Adjacent Lanes"), tr("Show the driving paths for the left and right lanes."), ""}, + {"BlindSpotPath", tr("Blind Spot Path"), tr("Show a red path when a vehicle is in that lane's blind spot."), ""}, + {"Compass", tr("Compass"), tr("Show the current driving direction with a simple on-screen compass."), ""}, + {"OnroadDistanceButton", tr("Driving Personality Button"), tr("Control and view the current driving personality via a driving screen widget."), ""}, + {"PedalsOnUI", tr("Gas / Brake Pedal Indicators"), tr("On-screen gas and brake indicators.

Dynamic: Opacity changes according to how much openpilot is accelerating or braking
Static: Full when active, dim when not"), ""}, + {"RotatingWheel", tr("Rotating Steering Wheel"), tr("Rotate the driving screen wheel with the physical steering wheel."), ""}, + + {"ModelUI", tr("Model UI"), tr("Model visualizations for the driving path, lane lines, path edges, and road edges."), "../../frogpilot/assets/toggle_icons/icon_road.png"}, + {"DynamicPathWidth", tr("Dynamic Path Width"), tr("Change the path width based on engagement.

Fully Engaged: 100%
Always On Lateral: 75%
Disengaged: 50%"), ""}, + {"LaneLinesWidth", tr("Lane Lines Width"), tr("Set the lane-line thickness.

Default matches the MUTCD lane-line width standard of 4 inches."), ""}, + {"PathEdgeWidth", tr("Path Edges Width"), tr("Set the driving-path edge width that represents different driving modes and statuses.

Default is 20% of the total path width.

Color Guide:

- Light Blue: Always On Lateral
- Green: Default
- Orange: Experimental Mode
- Red: Traffic Mode
- Yellow: Conditional Experimental Mode overridden"), ""}, + {"PathWidth", tr("Path Width"), tr("Set the driving-path width.

Default (6.1 feet) matches the width of a 2019 Lexus ES 350."), ""}, + {"RoadEdgesWidth", tr("Road Edges Width"), tr("Set the road-edge thickness.

Default matches half of the MUTCD lane-line width standard of 4 inches."), ""}, + + {"NavigationUI", tr("Navigation Widgets"), tr("Speed limits, and other navigation widgets."), "../../frogpilot/assets/toggle_icons/icon_map.png"}, + {"RoadNameUI", tr("Road Name"), tr("Display the road name at the bottom of the driving screen using data from \"OpenStreetMap (OSM)\"."), ""}, + {"ShowSpeedLimits", tr("Show Speed Limits"), tr("Show speed limits in the top-left corner of the driving screen. Uses data from the car's dashboard (if supported) and \"OpenStreetMap (OSM)\"."), ""}, + {"SLCMapboxFiller", tr("Show Speed Limits from Mapbox"), tr("Use Mapbox speed-limit data when no other source is available."), ""}, + {"UseVienna", tr("Use Vienna-Style Speed Signs"), tr("Show Vienna-style (EU) speed-limit signs instead of MUTCD (US)."), ""}, + + {"QOLVisuals", tr("Quality of Life"), tr("Miscellaneous visual changes to fine-tune how the driving screen looks."), "../../frogpilot/assets/toggle_icons/icon_quality_of_life.png"}, + {"CameraView", tr("Camera View"), tr("Select the active camera view. This is purely a visual change and doesn't impact how openpilot drives!"), ""}, + {"DriverCamera", tr("Show Driver Camera When In Reverse"), tr("Show the driver camera feed when the vehicle is in reverse."), ""}, + {"StoppedTimer", tr("Stopped Timer"), tr("Show a timer when stopped in place of the current speed to indicate how long the vehicle has been stopped."), ""} + }; + + for (const auto &[param, title, desc, icon] : visualToggles) { + AbstractControl *visualToggle; + + if (param == "AdvancedCustomUI") { + FrogPilotManageControl *advancedCustomUIToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(advancedCustomUIToggle, &FrogPilotManageControl::manageButtonClicked, [visualsLayout, advancedCustomPanel]() { + visualsLayout->setCurrentWidget(advancedCustomPanel); + }); + visualToggle = advancedCustomUIToggle; + + } else if (param == "CustomUI") { + FrogPilotManageControl *customUIToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(customUIToggle, &FrogPilotManageControl::manageButtonClicked, [visualsLayout, customUIPanel]() { + visualsLayout->setCurrentWidget(customUIPanel); + }); + visualToggle = customUIToggle; + } else if (param == "PedalsOnUI") { + std::vector pedalsToggles{"DynamicPedalsOnUI", "StaticPedalsOnUI"}; + std::vector pedalsToggleNames{tr("Dynamic"), tr("Static")}; + FrogPilotButtonToggleControl *pedalsToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, pedalsToggles, pedalsToggleNames, true); + QObject::connect(pedalsToggle, &FrogPilotButtonToggleControl::buttonClicked, [this](int id) { + if (id == 0) { + params.putBool("StaticPedalsOnUI", false); + } else if (id == 1) { + params.putBool("DynamicPedalsOnUI", false); + } + }); + visualToggle = pedalsToggle; + + } else if (param == "ModelUI") { + FrogPilotManageControl *modelUIToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(modelUIToggle, &FrogPilotManageControl::manageButtonClicked, [visualsLayout, modelUIPanel]() { + visualsLayout->setCurrentWidget(modelUIPanel); + }); + visualToggle = modelUIToggle; + } else if (param == "LaneLinesWidth" || param == "RoadEdgesWidth") { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 24, tr(" inches")); + } else if (param == "PathEdgeWidth") { + std::map pathEdgeLabels; + for (int i = 0; i <= 100; ++i) { + pathEdgeLabels[i] = i == 0 ? tr("Off") : QString::number(i) + "%"; + } + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 100, QString(), pathEdgeLabels); + } else if (param == "PathWidth") { + visualToggle = new FrogPilotParamValueControl(param, title, desc, icon, 0, 10, tr(" feet"), std::map(), 0.1); + + } else if (param == "NavigationUI") { + FrogPilotManageControl *navigationUIToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(navigationUIToggle, &FrogPilotManageControl::manageButtonClicked, [visualsLayout, navigationUIPanel]() { + visualsLayout->setCurrentWidget(navigationUIPanel); + }); + visualToggle = navigationUIToggle; + + } else if (param == "QOLVisuals") { + FrogPilotManageControl *qolToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(qolToggle, &FrogPilotManageControl::manageButtonClicked, [visualsLayout, qualityOfLifePanel]() { + visualsLayout->setCurrentWidget(qualityOfLifePanel); + }); + visualToggle = qolToggle; + } else if (param == "CameraView") { + std::vector cameraOptions{tr("Auto"), tr("Driver"), tr("Standard"), tr("Wide")}; + ButtonParamControl *cameraSelection = new ButtonParamControl(param, title, desc, icon, cameraOptions); + visualToggle = cameraSelection; + + } else { + visualToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = visualToggle; + + if (advancedCustomOnroadUIKeys.contains(param)) { + advancedCustomList->addItem(visualToggle); + } else if (customOnroadUIKeys.contains(param)) { + customUIList->addItem(visualToggle); + } else if (modelUIKeys.contains(param)) { + modelUIList->addItem(visualToggle); + } else if (navigationUIKeys.contains(param)) { + navigationUIList->addItem(visualToggle); + } else if (qualityOfLifeKeys.contains(param)) { + qualityOfLifeList->addItem(visualToggle); + } else { + visualsList->addItem(visualToggle); + + parentKeys.insert(param); + } + + if (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(visualToggle)) { + QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + emit openSubPanel(); + openDescriptions(forceOpenDescriptions, toggles); + }); + } + + QObject::connect(visualToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(visualToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + QSet forceUpdateKeys = {"HideLeadMarker", "ShowSpeedLimits"}; + for (const QString &key : forceUpdateKeys) { + QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, this, &FrogPilotVisualsPanel::updateToggles); + } + + openDescriptions(forceOpenDescriptions, toggles); + + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [visualsLayout, visualsPanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + visualsLayout->setCurrentWidget(visualsPanel); + }); + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubSubPanel, [this]() { + openDescriptions(forceOpenDescriptions, toggles); + }); + QObject::connect(parent, &FrogPilotSettingsWindow::updateMetric, this, &FrogPilotVisualsPanel::updateMetric); +} + +void FrogPilotVisualsPanel::showEvent(QShowEvent *event) { + updateToggles(); +} + +void FrogPilotVisualsPanel::updateMetric(bool metric, bool bootRun) { + static bool previousMetric; + if (metric != previousMetric && !bootRun) { + double distanceConversion = metric ? FOOT_TO_METER : METER_TO_FOOT; + double smallDistanceConversion = metric ? INCH_TO_CM : CM_TO_INCH; + + params.putIntNonBlocking("LaneLinesWidth", params.getInt("LaneLinesWidth") * smallDistanceConversion); + params.putIntNonBlocking("RoadEdgesWidth", params.getInt("RoadEdgesWidth") * smallDistanceConversion); + + params.putFloatNonBlocking("PathWidth", params.getFloat("PathWidth") * distanceConversion); + } + previousMetric = metric; + + static std::map imperialDistanceLabels; + static std::map imperialSmallDistanceLabels; + static std::map metricDistanceLabels; + static std::map metricSmallDistanceLabels; + + static bool labelsInitialized = false; + if (!labelsInitialized) { + for (int i = 0; i <= 10; ++i) { + imperialDistanceLabels[i] = i == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" foot") : QString::number(i) + tr(" feet"); + } + + for (int i = 0; i <= 24; ++i) { + imperialSmallDistanceLabels[i] = i == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" inch") : QString::number(i) + tr(" inches"); + } + + for (float i = 0.0f; i <= 3.0f; i += 0.1f) { + metricDistanceLabels[i] = i == 0.0f ? tr("Off") : i == 1.0 ? QString::number(i) + tr(" meter") : QString::number(i, 'f', 1) + tr(" meters"); + } + + for (int i = 0; i <= 60; ++i) { + metricSmallDistanceLabels[i] = i == 0 ? tr("Off") : i == 1 ? QString::number(i) + tr(" centimeter") : QString::number(i) + tr(" centimeters"); + } + + labelsInitialized = true; + } + + FrogPilotParamValueControl *laneLinesWidthToggle = static_cast(toggles["LaneLinesWidth"]); + FrogPilotParamValueControl *pathWidthToggle = static_cast(toggles["PathWidth"]); + FrogPilotParamValueControl *roadEdgesWidthToggle = static_cast(toggles["RoadEdgesWidth"]); + + if (metric) { + laneLinesWidthToggle->setDescription(tr("Set the lane-line thickness.

Default matches the MUTCD lane-line width standard of 10 centimeters.")); + pathWidthToggle->setDescription(tr("Set the driving-path width.

Default (1.9 meters) matches the width of a 2019 Lexus ES 350.")); + roadEdgesWidthToggle->setDescription(tr("Set the road-edge thickness.

Default matches half of the MUTCD lane-line width standard of 10 centimeters.")); + + laneLinesWidthToggle->updateControl(0, 60, metricSmallDistanceLabels); + roadEdgesWidthToggle->updateControl(0, 60, metricSmallDistanceLabels); + + pathWidthToggle->updateControl(0, 3, metricDistanceLabels); + } else { + laneLinesWidthToggle->setDescription(tr("Set the lane-line thickness.

Default matches the MUTCD lane-line width standard of 4 inches.")); + pathWidthToggle->setDescription(tr("Set the driving-path width.

Default (6.1 feet) matches the width of a 2019 Lexus ES 350.")); + roadEdgesWidthToggle->setDescription(tr("Set the road-edge thickness.

Default matches half of the MUTCD lane-line width standard of 4 inches.")); + + laneLinesWidthToggle->updateControl(0, 24, imperialSmallDistanceLabels); + roadEdgesWidthToggle->updateControl(0, 24, imperialSmallDistanceLabels); + + pathWidthToggle->updateControl(0, 10, imperialDistanceLabels); + } +} + +void FrogPilotVisualsPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "AccelerationPath") { + setVisible &= parent->hasOpenpilotLongitudinal; + } + + else if (key == "BlindSpotPath") { + setVisible &= parent->hasBSM; + } + + else if (key == "HideLeadMarker") { + setVisible &= parent->hasOpenpilotLongitudinal; + } + + else if (key == "HideSpeedLimit") { + setVisible &= parent->hasOpenpilotLongitudinal && params.getBool("SpeedLimitController"); + } + + else if (key == "OnroadDistanceButton") { + setVisible &= parent->hasOpenpilotLongitudinal; + } + + else if (key == "PedalsOnUI") { + setVisible &= parent->hasOpenpilotLongitudinal; + } + + else if (key == "ShowSpeedLimits") { + setVisible &= !params.getBool("SpeedLimitController") || !parent->hasOpenpilotLongitudinal; + } + + else if (key == "SLCMapboxFiller") { + setVisible &= params.getBool("ShowSpeedLimits"); + setVisible &= !params.getBool("SpeedLimitController") || !parent->hasOpenpilotLongitudinal; + setVisible &= !params.get("MapboxSecretKey").empty(); + } + + else if (key == "UseVienna") { + setVisible &= params.getBool("ShowSpeedLimits") || params.getBool("SpeedLimitController"); + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (advancedCustomOnroadUIKeys.contains(key)) { + toggles["AdvancedCustomUI"]->setVisible(true); + } else if (customOnroadUIKeys.contains(key)) { + toggles["CustomUI"]->setVisible(true); + } else if (modelUIKeys.contains(key)) { + toggles["ModelUI"]->setVisible(true); + } else if (navigationUIKeys.contains(key)) { + toggles["NavigationUI"]->setVisible(true); + } else if (qualityOfLifeKeys.contains(key)) { + toggles["QOLVisuals"]->setVisible(true); + } + } + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/visual_settings.h b/frogpilot/ui/qt/offroad/visual_settings.h new file mode 100644 index 000000000..9ea1d36b8 --- /dev/null +++ b/frogpilot/ui/qt/offroad/visual_settings.h @@ -0,0 +1,37 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotVisualsPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotVisualsPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +signals: + void openSubPanel(); + void openSubSubPanel(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateMetric(bool metric, bool bootRun); + void updateToggles(); + + bool forceOpenDescriptions; + + std::map toggles; + + QSet advancedCustomOnroadUIKeys = {"HideAlerts", "HideLeadMarker", "HideMaxSpeed", "HideSpeed", "HideSpeedLimit", "WheelSpeed"}; + QSet customOnroadUIKeys = {"AccelerationPath", "AdjacentPath", "BlindSpotPath", "Compass", "OnroadDistanceButton", "PedalsOnUI", "RotatingWheel"}; + QSet modelUIKeys = {"DynamicPathWidth", "LaneLinesWidth", "PathEdgeWidth", "PathWidth", "RoadEdgesWidth"}; + QSet navigationUIKeys = {"RoadNameUI", "ShowSpeedLimits", "SLCMapboxFiller", "UseVienna"}; + QSet qualityOfLifeKeys = {"CameraView", "DriverCamera", "StoppedTimer"}; + + QSet parentKeys; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/offroad/wheel_settings.cc b/frogpilot/ui/qt/offroad/wheel_settings.cc new file mode 100644 index 000000000..c59251f73 --- /dev/null +++ b/frogpilot/ui/qt/offroad/wheel_settings.cc @@ -0,0 +1,84 @@ +#include "frogpilot/ui/qt/offroad/wheel_settings.h" + +FrogPilotWheelPanel::FrogPilotWheelPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + const std::vector> wheelToggles { + {"DistanceButtonControl", tr("Distance Button"), tr("Action performed when the \"Distance\" button is pressed."), "../../frogpilot/assets/toggle_icons/icon_mute.png"}, + {"LongDistanceButtonControl", tr("Distance Button (Long Press)"), tr("Action performed when the \"Distance\" button is pressed for more than 0.5 seconds."), "../../frogpilot/assets/toggle_icons/icon_mute.png"}, + {"VeryLongDistanceButtonControl", tr("Distance Button (Very Long Press)"), tr("Action performed when the \"Distance\" button is pressed for more than 2.5 seconds."), "../../frogpilot/assets/toggle_icons/icon_mute.png"}, + {"LKASButtonControl", tr("LKAS Button"), tr("Action performed when the \"LKAS\" button is pressed."), "../../frogpilot/assets/toggle_icons/icon_mute.png"} + }; + + for (const auto &[param, title, desc, icon] : wheelToggles) { + QMap functionsMap { + {0, tr("No Action")}, + {3, tr("Pause Steering")} + }; + + QMap longitudinalFunctionsMap { + {1, tr("Change \"Personality Profile\"")}, + {2, tr("Force openpilot to Coast")}, + {4, tr("Pause Acceleration/Braking")}, + {5, tr("Toggle \"Experimental Mode\" On/Off")}, + {6, tr("Toggle \"Traffic Mode\" On/Off")} + }; + + ButtonControl *wheelToggle = new ButtonControl(title, tr("SELECT"), desc); + QObject::connect(wheelToggle, &ButtonControl::clicked, [functionsMap, longitudinalFunctionsMap, key = param, parent, wheelToggle, this]() mutable { + if (parent->hasOpenpilotLongitudinal) { + QMap::const_iterator it; + for (it = longitudinalFunctionsMap.constBegin(); it != longitudinalFunctionsMap.constEnd(); ++it) { + functionsMap[it.key()] = it.value(); + } + } + + QString selection = MultiOptionDialog::getSelection(tr("Select a function to assign to this button"), functionsMap.values(), functionsMap[params.getInt(key.toStdString())], this); + if (!selection.isEmpty()) { + params.putInt(key.toStdString(), functionsMap.key(selection)); + + wheelToggle->setValue(selection); + } + }); + QMap mergedFunctionsMap = functionsMap; + QMap::const_iterator it; + for (it = longitudinalFunctionsMap.constBegin(); it != longitudinalFunctionsMap.constEnd(); ++it) { + mergedFunctionsMap[it.key()] = it.value(); + } + wheelToggle->setValue(mergedFunctionsMap[params.getInt(param.toStdString())]); + + toggles[param] = wheelToggle; + + addItem(wheelToggle); + + QObject::connect(wheelToggle, &AbstractControl::hideDescriptionEvent, [this]() { + update(); + }); + QObject::connect(wheelToggle, &AbstractControl::showDescriptionEvent, [this]() { + update(); + }); + } + + openDescriptions(forceOpenDescriptions, toggles); +} + +void FrogPilotWheelPanel::showEvent(QShowEvent *event) { + updateToggles(); +} + +void FrogPilotWheelPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + if (key == "LKASButtonControl") { + setVisible &= !parent->isSubaru; + setVisible &= !parent->lkasAllowedForAOL || !(params.getBool("AlwaysOnLateral") && params.getBool("AlwaysOnLateralLKAS")); + } + + toggle->setVisible(setVisible); + } + + openDescriptions(forceOpenDescriptions, toggles); + + update(); +} diff --git a/frogpilot/ui/qt/offroad/wheel_settings.h b/frogpilot/ui/qt/offroad/wheel_settings.h new file mode 100644 index 000000000..458c72bd2 --- /dev/null +++ b/frogpilot/ui/qt/offroad/wheel_settings.h @@ -0,0 +1,24 @@ +#pragma once + +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class FrogPilotWheelPanel : public FrogPilotListWidget { + Q_OBJECT + +public: + explicit FrogPilotWheelPanel(FrogPilotSettingsWindow *parent, bool forceOpen = false); + +protected: + void showEvent(QShowEvent *event) override; + +private: + void updateToggles(); + + bool forceOpenDescriptions; + + std::map toggles; + + FrogPilotSettingsWindow *parent; + + Params params; +}; diff --git a/frogpilot/ui/qt/widgets/frogpilot_controls.cc b/frogpilot/ui/qt/widgets/frogpilot_controls.cc index e35184bfe..1edea9ca7 100644 --- a/frogpilot/ui/qt/widgets/frogpilot_controls.cc +++ b/frogpilot/ui/qt/widgets/frogpilot_controls.cc @@ -2,6 +2,16 @@ #include "frogpilot/ui/frogpilot_ui.h" +bool FrogPilotConfirmationDialog::toggleReboot(QWidget *parent) { + ConfirmationDialog d(tr("Reboot required to take effect."), tr("Reboot Now"), tr("Reboot Later"), false, parent); + return d.exec(); +} + +bool FrogPilotConfirmationDialog::yesorno(const QString &prompt_text, QWidget *parent) { + ConfirmationDialog d(prompt_text, tr("Yes"), tr("No"), false, parent); + return d.exec(); +} + void clearMovie(QSharedPointer &movie, QWidget *parent) { if (!movie) { return; @@ -92,3 +102,13 @@ void loadImage(const QString &basePath, QPixmap &pixmap, QSharedPointer parent->update(); } + +void openDescriptions(bool forceOpenDescriptions, std::map toggles) { + if (forceOpenDescriptions) { + for (auto &[key, toggle] : toggles) { + if (key != "CESpeed") { + toggle->showDescription(); + } + } + } +} diff --git a/frogpilot/ui/qt/widgets/frogpilot_controls.h b/frogpilot/ui/qt/widgets/frogpilot_controls.h index f930059c9..c73decb3b 100644 --- a/frogpilot/ui/qt/widgets/frogpilot_controls.h +++ b/frogpilot/ui/qt/widgets/frogpilot_controls.h @@ -17,3 +17,622 @@ void loadGif(const QString &gifPath, QSharedPointer &movie, const QSize &size, QWidget *parent); void loadImage(const QString &basePath, QPixmap &pixmap, QSharedPointer &movie, const QSize &size, QWidget *parent); +void openDescriptions(bool forceOpenDescriptions, std::map toggles); + +const QString buttonStyle = R"( + QPushButton { + padding: 0px 25px 0px 25px; + border-radius: 50px; + font-size: 35px; + font-weight: 500; + height: 100px; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } +)"; + +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 toggleReboot(QWidget *parent); + static bool yesorno(const QString &prompt_text, QWidget *parent); +}; + +class FrogPilotListWidget : public QWidget { + Q_OBJECT + public: + explicit FrogPilotListWidget(QWidget *parent = 0) : 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 + outer_layout.addStretch(); + } + inline void addItem(QWidget *w, bool expanding = false) { + w->setSizePolicy(QSizePolicy::Preferred, expanding ? QSizePolicy::Expanding : QSizePolicy::Fixed); + inner_layout.addWidget(w); + } + inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } + inline void insertItem(int index, QWidget *w, bool expanding = false) { + w->setSizePolicy(QSizePolicy::Preferred, expanding ? QSizePolicy::Expanding : QSizePolicy::Fixed); + inner_layout.insertWidget(index, w); + } + inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); } + + void clear() { + while (QLayoutItem *child = inner_layout.takeAt(0)) { + if (child->widget()) { + child->widget()->deleteLater(); + } + delete child; + } + outer_layout.addStretch(); + } + +private: + void paintEvent(QPaintEvent *) override { + QPainter p(this); + p.setPen(Qt::gray); + for (int i = 0; i < inner_layout.count() - 1; ++i) { + QWidget *widget = inner_layout.itemAt(i)->widget(); + + QWidget *nextWidget = nullptr; + for (int j = i + 1; j < inner_layout.count(); ++j) { + nextWidget = inner_layout.itemAt(j)->widget(); + if (nextWidget != nullptr && nextWidget->isVisible()) { + break; + } + } + + if (widget == nullptr || (widget->isVisible() && nextWidget->isVisible())) { + QRect r = inner_layout.itemAt(i)->geometry(); + int bottom = r.bottom() + inner_layout.spacing() / 2; + p.drawLine(r.left() + 40, bottom, r.right() - 40, bottom); + } + } + } + QVBoxLayout outer_layout; + QVBoxLayout inner_layout; +}; + +class FrogPilotButtonControl : public ParamControl { + Q_OBJECT +public: + FrogPilotButtonControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, bool checkable = false, + bool exclusive = false, int minimum_button_width = 225) : ParamControl(param, title, desc, icon) { + key = param.toStdString(); + + button_group = new QButtonGroup(this); + button_group->setExclusive(exclusive); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(checkable); + button->setStyleSheet(buttonStyle); + button->setMinimumWidth(minimum_button_width); + hlayout->addWidget(button); + button_group->addButton(button, i); + } + + hlayout->addWidget(&toggle); + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { + emit buttonClicked(id); + }); + + QObject::connect(this, &ToggleControl::toggleFlipped, this, &FrogPilotButtonControl::refresh); + + refresh(); + } + + virtual void refresh() { + bool state = params.getBool(key); + if (state != toggle.on) { + toggle.togglePosition(); + } + + for (QAbstractButton *button : button_group->buttons()) { + button->setEnabled(state); + } + } + + void clearCheckedButtons() { + bool original_exclusive = button_group->exclusive(); + + button_group->setExclusive(false); + + for (QAbstractButton *button : button_group->buttons()) { + button->setChecked(false); + } + + button_group->setExclusive(original_exclusive); + } + + void setCheckedButton(int id) { + if (QAbstractButton *button = button_group->button(id)) { + button->setChecked(true); + } + } + + void setEnabledButton(int id, bool enable) { + if (QAbstractButton *button = button_group->button(id)) { + button->setEnabled(enable); + } + } + + void setVisibleButton(int id, bool visible) { + if (QAbstractButton *button = button_group->button(id)) { + button->setVisible(visible); + } + } + +signals: + void buttonClicked(int id); + +protected: + std::string key; + + Params params; + + QButtonGroup *button_group; +}; + +class FrogPilotButtonsControl : public AbstractControl { + Q_OBJECT +public: + FrogPilotButtonsControl(const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, const bool &checkable = false, const bool &exclusive = true, + const int minimum_button_width = 225) : AbstractControl(title, desc, icon) { + button_group = new QButtonGroup(this); + button_group->setExclusive(exclusive); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->installEventFilter(this); + button->setCheckable(checkable); + button->setStyleSheet(buttonStyle); + button->setMinimumWidth(minimum_button_width); + hlayout->addWidget(button); + button_group->addButton(button, i); + } + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { + emit buttonClicked(id); + }); + } + + void clearCheckedButtons() { + bool original_exclusive = button_group->exclusive(); + + button_group->setExclusive(false); + + for (QAbstractButton *button : button_group->buttons()) { + button->setChecked(false); + } + + button_group->setExclusive(original_exclusive); + } + + void setCheckedButton(int id) { + if (QAbstractButton *button = button_group->button(id)) { + button->setChecked(true); + } + } + + void setEnabled(bool enable) { + for (QAbstractButton *button : button_group->buttons()) { + button->setEnabled(enable); + } + } + + void setEnabledButtons(int id, bool enable) { + if (QAbstractButton *button = button_group->button(id)) { + button->setEnabled(enable); + } + } + + void setText(int id, const QString &text) { + if (QAbstractButton *button = button_group->button(id)) { + button->setText(text); + } + } + + void setVisibleButton(int id, bool visible) { + if (QAbstractButton *button = button_group->button(id)) { + button->setVisible(visible); + } + } + +signals: + void buttonClicked(int id); + void disabledButtonClicked(int id); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override { + if (event->type() == QEvent::MouseButtonPress) { + QPushButton *button = qobject_cast(obj); + if (button && !button->isEnabled()) { + emit disabledButtonClicked(button_group->id(button)); + } + } + return AbstractControl::eventFilter(obj, event); + } + +private: + QButtonGroup *button_group; +}; + +class FrogPilotButtonToggleControl : public FrogPilotButtonControl { + Q_OBJECT +public: + FrogPilotButtonToggleControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector &button_params, const std::vector &button_texts, + bool exclusive = false, int minimum_button_width = 225) + : FrogPilotButtonControl(param, title, desc, icon, button_texts, true, exclusive, minimum_button_width), button_params(button_params) { + + for (int i = 0; i < button_texts.size(); i++) { + button_group->buttons()[i]->setChecked(params.getBool(button_params[i].toStdString())); + } + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { + params.putBool(button_params[id].toStdString(), button_group->button(id)->isChecked()); + emit buttonClicked(id); + }); + } + + void refresh() override { + FrogPilotButtonControl::refresh(); + + for (int i = 0; i < button_group->buttons().size(); i++) { + QAbstractButton *button = button_group->button(i); + button->setChecked(params.getBool(button_params[i].toStdString())); + } + } + +private: + std::vector button_params; +}; + +class FrogPilotManageControl : public ParamControl { + Q_OBJECT +public: + FrogPilotManageControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon) : ParamControl(param, title, desc, icon) { + key = param.toStdString(); + + manageButton = new ButtonControl("", tr("MANAGE"), "", this); + + hlayout->insertWidget(hlayout->indexOf(&toggle) - 1, manageButton); + + QObject::connect(manageButton, &ButtonControl::clicked, this, &FrogPilotManageControl::manageButtonClicked); + QObject::connect(this, &ToggleControl::toggleFlipped, this, &FrogPilotManageControl::refresh); + } + + void refresh() { + manageButton->setEnabled(params.getBool(key)); + } + + void setManageVisibility(bool visible) { + manageButton->setVisible(visible); + } + + void showEvent(QShowEvent *event) override { + refresh(); + ParamControl::showEvent(event); + } + +signals: + void manageButtonClicked(); + +private: + std::string key; + + ButtonControl *manageButton; + + Params params; +}; + +class FrogPilotParamValueControl : public AbstractControl { + Q_OBJECT +public: + FrogPilotParamValueControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + float min_value, float max_value, const QString &label, const std::map &value_labels = {}, + float interval = 1.0f, bool fast_increase = false, int label_width = 350) + : AbstractControl(title, desc, icon), + fast_increase(fast_increase), interval(interval), label(label), min_value(min_value), max_value(max_value), value_labels(value_labels) { + factor = std::pow(10, std::ceil(-std::log10(interval))); + key = param.toStdString(); + key_type = params.getKeyType(key); + + setupButton(decrement_button, "-"); + setupButton(increment_button, "+"); + + value_label = new QLabel(this); + value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + value_label->setFixedSize(QSize(label_width, 100)); + value_label->setStyleSheet("QLabel {color: #E0E879;}"); + + hlayout->addWidget(value_label); + hlayout->addWidget(&decrement_button); + hlayout->addWidget(&increment_button); + + QObject::connect(&decrement_button, &QPushButton::pressed, this, &FrogPilotParamValueControl::decrementPressed); + QObject::connect(&increment_button, &QPushButton::pressed, this, &FrogPilotParamValueControl::incrementPressed); + + last_action_timer.start(); + } + + void decrementPressed() { + if (display_warning && !warning_shown) { + showWarning(); + } + + if (last_action_timer.isValid() && last_action_timer.elapsed() > decrement_button.autoRepeatInterval() + 50) { + decrement_repeating = false; + } + + float delta = decrement_repeating && fast_increase ? interval * 5 : interval; + value = std::max(value - delta, min_value); + + updateValue(); + + if (std::lround(value / interval) % 5 == 0) { + decrement_repeating = true; + } + + last_action_timer.restart(); + } + + void hideEvent(QHideEvent *event) override { + AbstractControl::hideEvent(event); + + warning_shown = false; + + updateParam(); + } + + void incrementPressed() { + if (display_warning && !warning_shown) { + showWarning(); + } + + if (last_action_timer.isValid() && last_action_timer.elapsed() > increment_button.autoRepeatInterval() + 50) { + increment_repeating = false; + } + + float delta = increment_repeating && fast_increase ? interval * 5 : interval; + value = std::min(value + delta, max_value); + + updateValue(); + + if (std::lround(value / interval) % 5 == 0) { + increment_repeating = true; + } + + last_action_timer.restart(); + } + + void refresh() { + if (key_type == ParamKeyType::INT) { + value = std::clamp(std::round(params.getInt(key) * factor) / factor, min_value, max_value); + } else { + value = std::clamp(std::round(params.getFloat(key) * factor) / factor, min_value, max_value); + } + previous_value = value; + + updateDisplay(); + updateParam(); + } + + void setWarning(const QString &newWarning) { + display_warning = true; + + warning = newWarning; + } + + void setupButton(QPushButton &button, const QString &text) { + button.setAutoRepeat(true); + button.setAutoRepeatDelay(500); + button.setAutoRepeatInterval(150); + button.setFixedSize(150, 100); + button.setStyleSheet(buttonStyle); + if (text == "+" || text == "-") { + button.setStyleSheet(button.styleSheet() + " QPushButton { font-size: 50px; }"); + } + button.setText(text); + } + + void showEvent(QShowEvent *event) override { + refresh(); + } + + void showWarning() { + ConfirmationDialog::alert(warning, this); + + warning_shown = true; + } + + void updateControl(const float &newMinValue, const float &newMaxValue, const std::map &newValueLabels = {}) { + min_value = newMinValue; + max_value = newMaxValue; + + value_labels = newValueLabels; + + refresh(); + } + + void updateDisplay() { + QString displayText = QString::number(value) + label; + + for (const std::pair &entry : value_labels) { + if (std::lround(entry.first * factor) == std::lround(value * factor)) { + displayText = entry.second; + break; + } + } + + decrement_button.setEnabled(value > min_value); + increment_button.setEnabled(value < max_value); + + value_label->setText(displayText); + } + + void updateParam() { + if (value == previous_value) { + return; + } + + if (key_type == ParamKeyType::INT) { + params.putInt(key, value); + } else { + params.putFloat(key, value); + } + } + + void updateValue() { + value = std::round(value * factor) / factor; + + emit valueChanged(value); + + updateDisplay(); + } + +signals: + void valueChanged(float value); + +protected: + QLabel *value_label; + +private: + bool decrement_repeating; + bool display_warning; + bool fast_increase; + bool increment_repeating; + bool warning_shown; + + float interval; + float factor; + float max_value; + float min_value; + float previous_value; + float value; + + std::map value_labels; + + std::string key; + + ParamKeyType key_type; + + Params params; + + QElapsedTimer last_action_timer; + + QPushButton decrement_button; + QPushButton increment_button; + + QString label; + QString warning; +}; + +class FrogPilotParamValueButtonControl : public FrogPilotParamValueControl { + Q_OBJECT +public: + FrogPilotParamValueButtonControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + float min_value, float max_value, const QString &label, const std::map &value_labels, + float interval, bool fast_increase, const std::vector &button_params, const std::vector &button_texts, + bool left_button = false, bool checkable = true, int minimum_button_width = 225) + : FrogPilotParamValueControl(param, title, desc, icon, min_value, max_value, label, value_labels, interval, fast_increase, 200), + button_params(button_params), checkable(checkable) { + 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(checkable); + button->setChecked(checkable && params.getBool(button_params[i].toStdString())); + button->setStyleSheet(buttonStyle); + button->setMinimumWidth(minimum_button_width); + if (left_button) { + hlayout->insertWidget(hlayout->indexOf(value_label) - 1, button); + } else { + hlayout->addWidget(button); + } + button_group->addButton(button, i); + } + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonClicked), [=](int id) { + if (checkable) { + params.putBool(button_params[id].toStdString(), button_group->button(id)->isChecked()); + } + emit buttonClicked(id); + }); + } + + void refresh() { + if (checkable) { + for (int i = 0; i < button_group->buttons().size(); i++) { + QAbstractButton *button = button_group->button(i); + button->setChecked(params.getBool(button_params[i].toStdString())); + } + } + FrogPilotParamValueControl::refresh(); + } + + void showEvent(QShowEvent *event) override { + refresh(); + FrogPilotParamValueControl::showEvent(event); + } + +public: + void setEnabledButtons(int id, bool enable) { + if (QAbstractButton *button = button_group->button(id)) { + button->setEnabled(enable); + } + } + +signals: + void buttonClicked(int id); + +private: + bool checkable; + + std::vector button_params; + + Params params; + + QButtonGroup *button_group; +}; + +class FrogPilotDualParamValueControl : public QFrame { + Q_OBJECT +public: + FrogPilotDualParamValueControl(FrogPilotParamValueControl *control1, FrogPilotParamValueControl *control2, QWidget *parent = nullptr) : QFrame(parent), control1(control1), control2(control2) { + QHBoxLayout *hlayout = new QHBoxLayout(this); + hlayout->addWidget(control1); + hlayout->addWidget(control2); + } + + void updateControl(const float &newMinValue, const float &newMaxValue, const std::map &newValueLabels = {}) { + control1->updateControl(newMinValue, newMaxValue, newValueLabels); + control2->updateControl(newMinValue, newMaxValue, newValueLabels); + } + + void refresh() { + control1->refresh(); + control2->refresh(); + } + +private: + FrogPilotParamValueControl *control1; + FrogPilotParamValueControl *control2; +}; diff --git a/frogpilot/ui/qt/widgets/navigation_functions.cc b/frogpilot/ui/qt/widgets/navigation_functions.cc new file mode 100644 index 000000000..1ef7c8d36 --- /dev/null +++ b/frogpilot/ui/qt/widgets/navigation_functions.cc @@ -0,0 +1,62 @@ +#include +#include +#include + +#include "frogpilot/ui/qt/widgets/navigation_functions.h" + +MapSelectionControl::MapSelectionControl(const QMap &map, bool isCountry) : selectionType(isCountry ? "nations" : "states") { + mapButtons = new QButtonGroup(this); + mapButtons->setExclusive(false); + + QGridLayout *mapLayout = new QGridLayout(this); + + QList keys = map.keys(); + for (int i = 0; i < keys.size(); ++i) { + QPushButton *button = new QPushButton(map[keys[i]], this); + button->setCheckable(true); + button->setProperty("mapKey", keys[i]); + button->setStyleSheet(buttonStyle); + + mapButtons->addButton(button, i); + + mapLayout->addWidget(button, i / 3, i % 3); + + QObject::connect(button, &QPushButton::toggled, this, &MapSelectionControl::updateSelectedMaps); + } + + loadSelectedMaps(); +} + +void MapSelectionControl::loadSelectedMaps() { + QJsonObject existingMaps = QJsonDocument::fromJson(QByteArray::fromStdString(params.get("MapsSelected"))).object(); + + QSet selectedMaps; + for (const QJsonValue &value : existingMaps.value(selectionType).toArray()) { + selectedMaps.insert(value.toString()); + } + + for (QAbstractButton *button : mapButtons->buttons()) { + button->setChecked(selectedMaps.contains(button->property("mapKey").toString())); + } +} + +void MapSelectionControl::updateSelectedMaps() { + QJsonObject existingMaps = QJsonDocument::fromJson(QByteArray::fromStdString(params.get("MapsSelected"))).object(); + + QSet selectedMaps; + for (const QJsonValue &value : existingMaps.value(selectionType).toArray()) { + selectedMaps.insert(value.toString()); + } + + for (QAbstractButton *button : mapButtons->buttons()) { + QString mapKey = button->property("mapKey").toString(); + if (button->isChecked()) { + selectedMaps.insert(mapKey); + } else { + selectedMaps.remove(mapKey); + } + } + + existingMaps[selectionType] = QJsonArray::fromStringList(selectedMaps.values()); + params.putNonBlocking("MapsSelected", QJsonDocument(existingMaps).toJson(QJsonDocument::Compact).toStdString()); +} diff --git a/frogpilot/ui/qt/widgets/navigation_functions.h b/frogpilot/ui/qt/widgets/navigation_functions.h new file mode 100644 index 000000000..ccd28f514 --- /dev/null +++ b/frogpilot/ui/qt/widgets/navigation_functions.h @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include + +#include "frogpilot/ui/qt/widgets/frogpilot_controls.h" + +inline QMap midwestMap = { + {"IL", "Illinois"}, {"IN", "Indiana"}, {"IA", "Iowa"}, + {"KS", "Kansas"}, {"MI", "Michigan"}, {"MN", "Minnesota"}, + {"MO", "Missouri"}, {"NE", "Nebraska"}, {"ND", "North Dakota"}, + {"OH", "Ohio"}, {"SD", "South Dakota"}, {"WI", "Wisconsin"} +}; + +inline QMap northeastMap = { + {"CT", "Connecticut"}, {"ME", "Maine"}, {"MA", "Massachusetts"}, + {"NH", "New Hampshire"}, {"NJ", "New Jersey"}, {"NY", "New York"}, + {"PA", "Pennsylvania"}, {"RI", "Rhode Island"}, {"VT", "Vermont"} +}; + +inline QMap southMap = { + {"AL", "Alabama"}, {"AR", "Arkansas"}, {"DE", "Delaware"}, + {"DC", "District of Columbia"}, {"FL", "Florida"}, {"GA", "Georgia"}, + {"KY", "Kentucky"}, {"LA", "Louisiana"}, {"MD", "Maryland"}, + {"MS", "Mississippi"}, {"NC", "North Carolina"}, {"OK", "Oklahoma"}, + {"SC", "South Carolina"}, {"TN", "Tennessee"}, {"TX", "Texas"}, + {"VA", "Virginia"}, {"WV", "West Virginia"} +}; + +inline QMap westMap = { + {"AK", "Alaska"}, {"AZ", "Arizona"}, {"CA", "California"}, + {"CO", "Colorado"}, {"HI", "Hawaii"}, {"ID", "Idaho"}, + {"MT", "Montana"}, {"NV", "Nevada"}, {"NM", "New Mexico"}, + {"OR", "Oregon"}, {"UT", "Utah"}, {"WA", "Washington"}, + {"WY", "Wyoming"} +}; + +inline QMap territoriesMap = { + {"AS", "American Samoa"}, {"GU", "Guam"}, {"MP", "Northern Mariana Islands"}, + {"PR", "Puerto Rico"}, {"VI", "Virgin Islands"} +}; + +inline QMap africaMap = { + {"DZ", "Algeria"}, {"AO", "Angola"}, {"BJ", "Benin"}, + {"BW", "Botswana"}, {"BF", "Burkina Faso"}, {"BI", "Burundi"}, + {"CM", "Cameroon"}, {"CF", "Central African Republic"}, {"TD", "Chad"}, + {"KM", "Comoros"}, {"CG", "Congo (Brazzaville)"}, {"CD", "Congo (Kinshasa)"}, + {"DJ", "Djibouti"}, {"EG", "Egypt"}, {"GQ", "Equatorial Guinea"}, + {"ER", "Eritrea"}, {"ET", "Ethiopia"}, {"GA", "Gabon"}, + {"GM", "Gambia"}, {"GH", "Ghana"}, {"GN", "Guinea"}, + {"GW", "Guinea-Bissau"}, {"CI", "Ivory Coast"}, {"KE", "Kenya"}, + {"LS", "Lesotho"}, {"LR", "Liberia"}, {"LY", "Libya"}, + {"MG", "Madagascar"}, {"MW", "Malawi"}, {"ML", "Mali"}, + {"MR", "Mauritania"}, {"MA", "Morocco"}, {"MZ", "Mozambique"}, + {"NA", "Namibia"}, {"NE", "Niger"}, {"NG", "Nigeria"}, + {"RW", "Rwanda"}, {"SN", "Senegal"}, {"SL", "Sierra Leone"}, + {"SO", "Somalia"}, {"ZA", "South Africa"}, {"SS", "South Sudan"}, + {"SD", "Sudan"}, {"SZ", "Swaziland"}, {"TZ", "Tanzania"}, + {"TG", "Togo"}, {"TN", "Tunisia"}, {"UG", "Uganda"}, + {"ZM", "Zambia"}, {"ZW", "Zimbabwe"} +}; + +inline QMap antarcticaMap = { + {"AQ", "Antarctica"} +}; + +inline QMap asiaMap = { + {"AF", "Afghanistan"}, {"AM", "Armenia"}, {"AZ", "Azerbaijan"}, + {"BH", "Bahrain"}, {"BD", "Bangladesh"}, {"BT", "Bhutan"}, + {"BN", "Brunei"}, {"KH", "Cambodia"}, {"CN", "China"}, + {"CY", "Cyprus"}, {"TL", "East Timor"}, {"HK", "Hong Kong"}, + {"IN", "India"}, {"ID", "Indonesia"}, {"IR", "Iran"}, + {"IQ", "Iraq"}, {"IL", "Israel"}, {"JP", "Japan"}, + {"JO", "Jordan"}, {"KZ", "Kazakhstan"}, {"KW", "Kuwait"}, + {"KG", "Kyrgyzstan"}, {"LA", "Laos"}, {"LB", "Lebanon"}, + {"MY", "Malaysia"}, {"MV", "Maldives"}, {"MO", "Macao"}, + {"MN", "Mongolia"}, {"MM", "Myanmar"}, {"NP", "Nepal"}, + {"KP", "North Korea"}, {"OM", "Oman"}, {"PK", "Pakistan"}, + {"PS", "Palestine"}, {"PH", "Philippines"}, {"QA", "Qatar"}, + {"RU", "Russia"}, {"SA", "Saudi Arabia"}, {"SG", "Singapore"}, + {"KR", "South Korea"}, {"LK", "Sri Lanka"}, {"SY", "Syria"}, + {"TW", "Taiwan"}, {"TJ", "Tajikistan"}, {"TH", "Thailand"}, + {"TR", "Turkey"}, {"TM", "Turkmenistan"}, {"AE", "United Arab Emirates"}, + {"UZ", "Uzbekistan"}, {"VN", "Vietnam"}, {"YE", "Yemen"} +}; + +inline QMap europeMap = { + {"AL", "Albania"}, {"AT", "Austria"}, {"BY", "Belarus"}, + {"BE", "Belgium"}, {"BA", "Bosnia and Herzegovina"}, {"BG", "Bulgaria"}, + {"HR", "Croatia"}, {"CZ", "Czech Republic"}, {"DK", "Denmark"}, + {"EE", "Estonia"}, {"FI", "Finland"}, {"FR", "France"}, + {"GE", "Georgia"}, {"DE", "Germany"}, {"GR", "Greece"}, + {"HU", "Hungary"}, {"IS", "Iceland"}, {"IE", "Ireland"}, + {"IT", "Italy"}, {"KZ", "Kazakhstan"}, {"LV", "Latvia"}, + {"LT", "Lithuania"}, {"LU", "Luxembourg"}, {"MK", "Macedonia"}, + {"MD", "Moldova"}, {"ME", "Montenegro"}, {"NL", "Netherlands"}, + {"NO", "Norway"}, {"PL", "Poland"}, {"PT", "Portugal"}, + {"RO", "Romania"}, {"RS", "Serbia"}, {"SK", "Slovakia"}, + {"SI", "Slovenia"}, {"ES", "Spain"}, {"SE", "Sweden"}, + {"CH", "Switzerland"}, {"TR", "Turkey"}, {"UA", "Ukraine"}, + {"GB", "United Kingdom"} +}; + +inline QMap northAmericaMap = { + {"BS", "Bahamas"}, {"BZ", "Belize"}, {"CA", "Canada"}, + {"CR", "Costa Rica"}, {"CU", "Cuba"}, {"DO", "Dominican Republic"}, + {"SV", "El Salvador"}, {"GL", "Greenland"}, {"GD", "Grenada"}, + {"GT", "Guatemala"}, {"HT", "Haiti"}, {"HN", "Honduras"}, + {"JM", "Jamaica"}, {"MX", "Mexico"}, {"NI", "Nicaragua"}, + {"PA", "Panama"}, {"TT", "Trinidad and Tobago"}, {"US", "United States"} +}; + +inline QMap oceaniaMap = { + {"AU", "Australia"}, {"FJ", "Fiji"}, {"TF", "French Southern Territories"}, + {"NC", "New Caledonia"}, {"NZ", "New Zealand"}, {"PG", "Papua New Guinea"}, + {"SB", "Solomon Islands"}, {"VU", "Vanuatu"} +}; + +inline QMap southAmericaMap = { + {"AR", "Argentina"}, {"BO", "Bolivia"}, {"BR", "Brazil"}, + {"CL", "Chile"}, {"CO", "Colombia"}, {"EC", "Ecuador"}, + {"FK", "Falkland Islands"}, {"GY", "Guyana"}, {"PY", "Paraguay"}, + {"PE", "Peru"}, {"SR", "Suriname"}, {"UY", "Uruguay"}, + {"VE", "Venezuela"} +}; + +inline QString calculateDirectorySize(const QDir &directory) { + constexpr double MB = 1024.0 * 1024.0; + constexpr double GB = 1024.0 * MB; + + if (!directory.exists()) { + return QObject::tr("0 MB"); + } + + double totalSize = 0; + QDirIterator it(directory.absolutePath(), QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + totalSize += it.fileInfo().size(); + } + + if (totalSize >= GB) { + return QString::number(totalSize / GB, 'f', 2) + QObject::tr(" GB"); + } + return QString::number(totalSize / MB, 'f', 2) + QObject::tr(" MB"); +} + +inline QString daySuffix(int day) { + if (day % 10 == 1 && day != 11) return "st"; + if (day % 10 == 2 && day != 12) return "nd"; + if (day % 10 == 3 && day != 13) return "rd"; + return "th"; +} + +inline QString formatCurrentDate() { + QDate currentDate = QDate::currentDate(); + return currentDate.toString("MMMM d'") + daySuffix(currentDate.day()) + QString(", %1").arg(currentDate.year()); +} + +inline QString formatElapsedTime(float elapsedMilliseconds) { + int totalSeconds = elapsedMilliseconds / 1000; + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int seconds = totalSeconds % 60; + + QString formattedTime; + if (hours > 0) { + formattedTime += QString::number(hours) + (hours == 1 ? QObject::tr(" hour ") : QObject::tr(" hours ")); + } + if (minutes > 0) { + formattedTime += QString::number(minutes) + (minutes == 1 ? QObject::tr(" minute ") : QObject::tr(" minutes ")); + } + formattedTime += QString::number(seconds) + (seconds == 1 ? QObject::tr(" second") : QObject::tr(" seconds")); + + return formattedTime.trimmed(); +} + +inline QString formatETA(float elapsedTime, int downloadedFiles, int previousDownloadedFiles, int totalFiles, QDateTime &startTime) { + static QDateTime estimatedFinishTime; + + static float previousElapsedTime; + + if (downloadedFiles != previousDownloadedFiles) { + estimatedFinishTime = startTime.addMSecs((elapsedTime * totalFiles) / downloadedFiles); + } else { + estimatedFinishTime = estimatedFinishTime.addSecs((previousElapsedTime - elapsedTime) / 1000); + } + previousElapsedTime = elapsedTime; + + int remainingTime = QDateTime::currentDateTime().secsTo(estimatedFinishTime); + + QString estimatedFinishTimeStr = estimatedFinishTime.toString("h:mm AP"); + QString remainingTimeStr = formatElapsedTime(remainingTime * 1000); + + return QString("%1 (%2)").arg(remainingTimeStr).arg(estimatedFinishTimeStr); +} + +class MapSelectionControl : public QWidget { + Q_OBJECT + +public: + MapSelectionControl(const QMap &map, bool isCountry = false); + +private: + void loadSelectedMaps(); + void updateSelectedMaps(); + + Params params; + + QButtonGroup *mapButtons; + + QString selectionType; +}; diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index d89d991af..415c13594 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -39,8 +39,16 @@ qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc", "qt/onroad/buttons.cc", "qt/onroad/alerts.cc", "qt/onroad/driver_monitoring.cc", "qt/onroad/hud.cc"] # FrogPilot variables -frogpilot_src = ["../../frogpilot/ui/frogpilot_ui.cc", "../../frogpilot/ui/qt/onroad/frogpilot_annotated_camera.cc", - "../../frogpilot/ui/qt/onroad/frogpilot_buttons.cc", "../../frogpilot/ui/qt/onroad/frogpilot_onroad.cc"] +frogpilot_src = ["../../frogpilot/ui/frogpilot_ui.cc", "../../frogpilot/ui/qt/offroad/data_settings.cc", + "../../frogpilot/ui/qt/offroad/device_settings.cc", "../../frogpilot/ui/qt/offroad/frogpilot_settings.cc", + "../../frogpilot/ui/qt/offroad/lateral_settings.cc", "../../frogpilot/ui/qt/offroad/longitudinal_settings.cc", + "../../frogpilot/ui/qt/offroad/maps_settings.cc", "../../frogpilot/ui/qt/offroad/model_settings.cc", + "../../frogpilot/ui/qt/offroad/navigation_settings.cc", "../../frogpilot/ui/qt/offroad/sounds_settings.cc", + "../../frogpilot/ui/qt/offroad/theme_settings.cc", "../../frogpilot/ui/qt/offroad/utilities.cc", + "../../frogpilot/ui/qt/offroad/vehicle_settings.cc", "../../frogpilot/ui/qt/offroad/visual_settings.cc", + "../../frogpilot/ui/qt/offroad/wheel_settings.cc", "../../frogpilot/ui/qt/onroad/frogpilot_annotated_camera.cc", + "../../frogpilot/ui/qt/onroad/frogpilot_buttons.cc", "../../frogpilot/ui/qt/onroad/frogpilot_onroad.cc", + "../../frogpilot/ui/qt/widgets/navigation_functions.cc"] qt_src += frogpilot_src # build translation files diff --git a/selfdrive/ui/qt/network/wifi_manager.h b/selfdrive/ui/qt/network/wifi_manager.h index 76baec3bd..a0fdfa4ee 100644 --- a/selfdrive/ui/qt/network/wifi_manager.h +++ b/selfdrive/ui/qt/network/wifi_manager.h @@ -70,6 +70,7 @@ public: QString getTetheringPassword(); // FrogPilot variables + QString getIp4Address(); private: QString adapter; // Path to network manager wifi-device @@ -83,7 +84,6 @@ private: QString getAdapter(const uint = NM_DEVICE_TYPE_WIFI); uint getAdapterType(const QDBusObjectPath &path); - QString getIp4Address(); void deactivateConnectionBySsid(const QString &ssid); void deactivateConnection(const QDBusObjectPath &path); QVector getActiveConnections(); diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index 9370d7cfd..573fbd419 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -2,28 +2,39 @@ #include "selfdrive/ui/qt/widgets/ssh_keys.h" #include "selfdrive/ui/qt/widgets/controls.h" -DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { +DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : QFrame(parent) { + mainLayout = new QStackedLayout(this); + + mainWidget = new QWidget(this); + QVBoxLayout *mainListLayout = new QVBoxLayout(mainWidget); + + mainListLayout->setContentsMargins(50, 25, 50, 25); + mainListLayout->setSpacing(20); + + FrogPilotListWidget *mainList = new FrogPilotListWidget(mainWidget); + mainListLayout->addWidget(mainList); + adbToggle = new ParamControl("AdbEnabled", tr("Enable ADB"), tr("ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info."), ""); - addItem(adbToggle); + mainList->addItem(adbToggle); // SSH keys - addItem(new SshToggle()); - addItem(new SshControl()); + mainList->addItem(new SshToggle()); + mainList->addItem(new SshControl()); joystickToggle = new ParamControl("JoystickDebugMode", tr("Joystick Debug Mode"), "", ""); QObject::connect(joystickToggle, &ParamControl::toggleFlipped, [=](bool state) { params.putBool("LongitudinalManeuverMode", false); longManeuverToggle->refresh(); }); - addItem(joystickToggle); + mainList->addItem(joystickToggle); longManeuverToggle = new ParamControl("LongitudinalManeuverMode", tr("Longitudinal Maneuver Mode"), "", ""); QObject::connect(longManeuverToggle, &ParamControl::toggleFlipped, [=](bool state) { params.putBool("JoystickDebugMode", false); joystickToggle->refresh(); }); - addItem(longManeuverToggle); + mainList->addItem(longManeuverToggle); experimentalLongitudinalToggle = new ParamControl( "AlphaLongitudinalEnabled", @@ -38,7 +49,7 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { QObject::connect(experimentalLongitudinalToggle, &ParamControl::toggleFlipped, [=]() { updateToggles(offroad); }); - addItem(experimentalLongitudinalToggle); + mainList->addItem(experimentalLongitudinalToggle); // Joystick and longitudinal maneuvers should be hidden on release branches is_release = params.getBool("IsReleaseBranch"); @@ -47,6 +58,201 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanel::updateToggles); // FrogPilot variables + QJsonObject shownDescriptions = QJsonDocument::fromJson(QString::fromStdString(params.get("ShownToggleDescriptions")).toUtf8()).object(); + QString className = this->metaObject()->className(); + + if (!shownDescriptions.value(className).toBool(false)) { + forceOpenDescriptions = true; + } + + std::vector keys = params.allKeys(); + for (const std::string &key : keys) { + frogpilotToggleLevels[QString::fromStdString(key)] = params.getTuningLevel(key); + } + + developerUIToggle = new FrogPilotManageControl("DeveloperUI", tr("Developer UI"), tr("Detailed information about openpilot's internal operations."), ""); + QObject::connect(developerUIToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + mainLayout->setCurrentWidget(developerUIPanel); + emit openSubPanel(); + }); + mainList->insertItem(0, developerUIToggle); + + mainLayout->addWidget(mainWidget); + + FrogPilotListWidget *developerUIList = new FrogPilotListWidget(this); + developerUIList->setContentsMargins(50, 25, 50, 25); + developerUIPanel = new ScrollView(developerUIList, this); + mainLayout->addWidget(developerUIPanel); + + FrogPilotListWidget *developerMetricList = new FrogPilotListWidget(this); + developerMetricList->setContentsMargins(50, 25, 50, 25); + developerMetricPanel = new ScrollView(developerMetricList, this); + mainLayout->addWidget(developerMetricPanel); + + FrogPilotListWidget *developerSidebarList = new FrogPilotListWidget(this); + developerSidebarList->setContentsMargins(50, 25, 50, 25); + developerSidebarPanel = new ScrollView(developerSidebarList, this); + mainLayout->addWidget(developerSidebarPanel); + + FrogPilotListWidget *developerWidgetList = new FrogPilotListWidget(this); + developerWidgetList->setContentsMargins(50, 25, 50, 25); + developerWidgetPanel = new ScrollView(developerWidgetList, this); + mainLayout->addWidget(developerWidgetPanel); + + std::vector> developerToggles { + {"DeveloperMetrics", tr("Developer Metrics"), tr("Performance data, sensor readings, and system metrics for debugging and optimizing openpilot."), ""}, + {"AdjacentPathMetrics", tr("Adjacent Lane Metrics"), tr("Show the width of the adjacent lanes."), ""}, + {"BorderMetrics", tr("Border Metrics"), tr("Show statuses along the border of the driving screen.

Blind Spot: The border turns red when a vehicle is in a blind spot
Steering Torque: The border goes from green to red according to how much steering torque is being used
Turn Signal: The border flashes yellow when a turn signal is on"), ""}, + {"FPSCounter", tr("FPS Display"), tr("Show the frames per second (FPS) at the bottom of the driving screen."), ""}, + {"LeadInfo", tr("Lead Info"), tr("Show each tracked vehicle's distance and speed below its marker."), ""}, + {"NumericalTemp", tr("Numerical Temperature Gauge"), tr("Show a numerical temperature in the sidebar instead of the status labels."), ""}, + {"SidebarMetrics", tr("Sidebar Metrics"), tr("Display system information (CPU, GPU, RAM usage, IP address, device storage) in the sidebar."), ""}, + {"UseSI", tr("Use International System of Units"), tr("Display measurements using the \"International System of Units\" (SI) standard."), ""}, + + {"DeveloperSidebar", tr("Developer Sidebar"), tr("Display debugging info and metrics in a dedicated sidebar on the right side of the screen."), ""}, + {"DeveloperSidebarMetric1", tr("Metric #1"), tr("Select the metric shown in the first \"Developer Sidebar\" widget."), ""}, + {"DeveloperSidebarMetric2", tr("Metric #2"), tr("Select the metric shown in the second \"Developer Sidebar\" widget."), ""}, + {"DeveloperSidebarMetric3", tr("Metric #3"), tr("Select the metric shown in the third \"Developer Sidebar\" widget."), ""}, + {"DeveloperSidebarMetric4", tr("Metric #4"), tr("Select the metric shown in the fourth \"Developer Sidebar\" widget."), ""}, + {"DeveloperSidebarMetric5", tr("Metric #5"), tr("Select the metric shown in the fifth \"Developer Sidebar\" widget."), ""}, + {"DeveloperSidebarMetric6", tr("Metric #6"), tr("Select the metric shown in the sixth \"Developer Sidebar\" widget."), ""}, + {"DeveloperSidebarMetric7", tr("Metric #7"), tr("Select the metric shown in the seventh \"Developer Sidebar\" widget."), ""}, + + {"DeveloperWidgets", tr("Developer Widgets"), tr("Overlays for debugging visuals, internal states, and model predictions on the driving screen."), ""}, + {"AdjacentLeadsUI", tr("Adjacent Leads Tracking"), tr("Display adjacent leads detected by the car's radar to the left and right of the current driving path."), ""}, + {"ShowStoppingPoint", tr("Model Stopping Point"), tr("Show a stop-sign marker where the model intends to stop."), ""}, + {"RadarTracksUI", tr("Radar Tracks"), tr("Display all radar points produced by the car's radar."), ""}, + }; + + for (const auto &[param, title, desc, icon] : developerToggles) { + AbstractControl *developerToggle; + + if (param == "DeveloperMetrics") { + FrogPilotManageControl *developerMetricsToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(developerMetricsToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + mainLayout->setCurrentWidget(developerMetricPanel); + emit openSubSubPanel(); + }); + developerToggle = developerMetricsToggle; + } else if (param == "BorderMetrics") { + std::vector borderToggles{"BlindSpotMetrics", "ShowSteering", "SignalMetrics"}; + std::vector borderToggleNames{tr("Blind Spot"), tr("Steering Torque"), tr("Turn Signal")}; + borderMetricsButton = new FrogPilotButtonToggleControl(param, title, desc, icon, borderToggles, borderToggleNames); + developerToggle = borderMetricsButton; + } else if (param == "NumericalTemp") { + std::vector temperatureToggles{"Fahrenheit"}; + std::vector temperatureToggleNames{tr("Fahrenheit")}; + developerToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, temperatureToggles, temperatureToggleNames); + } else if (param == "SidebarMetrics") { + sidebarMetricsToggles = {"ShowCPU", "ShowGPU", "ShowIP", "ShowMemoryUsage", "ShowStorageLeft", "ShowStorageUsed"}; + std::vector sidebarMetricsToggleNames{tr("CPU"), tr("GPU"), tr("IP"), tr("RAM"), tr("SSD Left"), tr("SSD Used")}; + sidebarMetricsToggle = new FrogPilotButtonsControl(title, desc, icon, sidebarMetricsToggleNames, true, false, 150); + for (int i = 0; i < sidebarMetricsToggles.size(); ++i) { + if (params.getBool(sidebarMetricsToggles[i].toStdString())) { + sidebarMetricsToggle->setCheckedButton(i); + } + } + QObject::connect(sidebarMetricsToggle, &FrogPilotButtonsControl::buttonClicked, [this](int id) { + params.putBool(sidebarMetricsToggles[id].toStdString(), !params.getBool(sidebarMetricsToggles[id].toStdString())); + + if (id == 0) { + params.putBool("ShowGPU", false); + } else if (id == 1) { + params.putBool("ShowCPU", false); + } else if (id == 3) { + params.putBool("ShowStorageLeft", false); + params.putBool("ShowStorageUsed", false); + } else if (id == 4) { + params.putBool("ShowMemoryUsage", false); + params.putBool("ShowStorageUsed", false); + } else if (id == 5) { + params.putBool("ShowMemoryUsage", false); + params.putBool("ShowStorageLeft", false); + } + + sidebarMetricsToggle->clearCheckedButtons(); + for (int i = 0; i < sidebarMetricsToggles.size(); ++i) { + if (params.getBool(sidebarMetricsToggles[i].toStdString())) { + sidebarMetricsToggle->setCheckedButton(i); + } + } + }); + developerToggle = sidebarMetricsToggle; + } else if (param == "DeveloperSidebar") { + FrogPilotManageControl *developerSidebarToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(developerSidebarToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + mainLayout->setCurrentWidget(developerSidebarPanel); + emit openSubSubPanel(); + }); + developerToggle = developerSidebarToggle; + } else if (developerSidebarKeys.contains(param)) { + QMap developerSidebarMetricOptions { + {0, tr("None")}, + {1, tr("Acceleration: Current")}, + {2, tr("Acceleration: Max")}, + {3, tr("Auto Tune: Actuator Delay")}, + {4, tr("Auto Tune: Friction")}, + {5, tr("Auto Tune: Lateral Acceleration")}, + {6, tr("Auto Tune: Steer Ratio")}, + {7, tr("Auto Tune: Stiffness Factor")}, + {8, tr("Engagement %: Lateral")}, + {9, tr("Engagement %: Longitudinal")}, + {10, tr("Lateral Control: Steering Angle")}, + {11, tr("Lateral Control: Torque % Used")}, + {12, tr("Longitudinal Control: Actuator Acceleration Output")}, + {13, tr("Longitudinal MPC: Danger Factor")}, + {14, tr("Longitudinal MPC Jerk: Acceleration")}, + {15, tr("Longitudinal MPC Jerk: Danger Zone")}, + {16, tr("Longitudinal MPC Jerk: Speed Control")}, + }; + + ButtonControl *metricToggle = new ButtonControl(title, tr("SELECT"), desc); + QObject::connect(metricToggle, &ButtonControl::clicked, [metricToggle, key = param, developerSidebarMetricOptions, this]() mutable { + QString current = developerSidebarMetricOptions.value(params.getInt(key.toStdString()), tr("None")); + QString selection = MultiOptionDialog::getSelection(tr("Select a metric to display"), developerSidebarMetricOptions.values(), current, this); + + if (!selection.isEmpty()) { + int selectedMetric = developerSidebarMetricOptions.key(selection); + + params.putInt(key.toStdString(), selectedMetric); + + metricToggle->setValue(selection); + } + }); + metricToggle->setValue(developerSidebarMetricOptions.value(params.getInt(param.toStdString()), tr("None"))); + developerToggle = metricToggle; + } else if (param == "DeveloperWidgets") { + FrogPilotManageControl *developerWidgetsToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(developerWidgetsToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { + mainLayout->setCurrentWidget(developerWidgetPanel); + emit openSubSubPanel(); + }); + developerToggle = developerWidgetsToggle; + } else if (param == "ShowStoppingPoint") { + std::vector stoppingPointToggles{"ShowStoppingPointMetrics"}; + std::vector stoppingPointToggleNames{tr("Show Distance")}; + developerToggle = new FrogPilotButtonToggleControl(param, title, desc, icon, stoppingPointToggles, stoppingPointToggleNames); + } else { + developerToggle = new ParamControl(param, title, desc, icon); + } + + toggles[param] = developerToggle; + + if (developerMetricKeys.contains(param)) { + developerMetricList->addItem(developerToggle); + } else if (developerSidebarKeys.contains(param)) { + developerSidebarList->addItem(developerToggle); + } else if (developerWidgetKeys.contains(param)) { + developerWidgetList->addItem(developerToggle); + } else { + developerUIList->addItem(developerToggle); + + parentKeys.insert(param); + } + } + + QObject::connect(parent, &SettingsWindow::closeSubPanel, [this]() {mainLayout->setCurrentWidget(mainWidget);}); + QObject::connect(parent, &SettingsWindow::closeSubSubPanel, [this]() {mainLayout->setCurrentWidget(developerUIPanel);}); } void DeveloperPanel::updateToggles(bool _offroad) { @@ -63,6 +269,15 @@ void DeveloperPanel::updateToggles(bool _offroad) { } // FrogPilot variables + for (auto &[key, toggle] : toggles) { + if (toggle == btn) { + btn->setEnabled(true); + break; + } + } + if (developerUIToggle == btn) { + btn->setEnabled(true); + } } // longManeuverToggle and experimentalLongitudinalToggle should not be toggleable if the car does not have longitudinal control @@ -87,6 +302,10 @@ void DeveloperPanel::updateToggles(bool _offroad) { longManeuverToggle->setEnabled(hasLongitudinalControl(CP) && _offroad); // FrogPilot variables + hasOpenpilotLongitudinal = hasLongitudinalControl(CP); + hasRadar = !CP.getRadarUnavailable(); + + borderMetricsButton->setVisibleButton(0, CP.getEnableBsm()); } else { longManeuverToggle->setEnabled(false); experimentalLongitudinalToggle->setVisible(false); @@ -96,10 +315,72 @@ void DeveloperPanel::updateToggles(bool _offroad) { offroad = _offroad; // FrogPilot variables + tuningLevel = params.getInt("TuningLevel"); + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = tuningLevel >= frogpilotToggleLevels[key].toDouble(); + + if (key == "AdjacentLeadsUI") { + setVisible &= hasRadar && !(params.getBool("AdvancedCustomUI") && params.getBool("HideLeadMarker")); + } + + else if (key == "RadarTracksUI") { + setVisible &= hasRadar; + } + + else if (key == "ShowStoppingPoint") { + setVisible &= hasOpenpilotLongitudinal; + } + + toggle->setVisible(setVisible); + + if (setVisible) { + if (developerMetricKeys.contains(key)) { + toggles["DeveloperMetrics"]->setVisible(true); + } else if (developerSidebarKeys.contains(key)) { + toggles["DeveloperSidebar"]->setVisible(true); + } else if (developerUIKeys.contains(key)) { + toggles["DeveloperUI"]->setVisible(true); + } else if (developerWidgetKeys.contains(key)) { + toggles["DeveloperWidgets"]->setVisible(true); + } + } + } + + borderMetricsButton->setVisibleButton(0, hasBSM); + + developerUIToggle->setVisible(tuningLevel >= frogpilotToggleLevels["DeveloperUI"].toDouble()); + + openDescriptions(forceOpenDescriptions, toggles); + + update(); } void DeveloperPanel::showEvent(QShowEvent *event) { updateToggles(offroad); // FrogPilot variables + for (int i = 0; i < sidebarMetricsToggles.size(); ++i) { + if (params.getBool(sidebarMetricsToggles[i].toStdString())) { + sidebarMetricsToggle->setCheckedButton(i); + } + } + + QJsonObject shownDescriptions = QJsonDocument::fromJson(QString::fromStdString(params.get("ShownToggleDescriptions")).toUtf8()).object(); + QString className = this->metaObject()->className(); + + if (!shownDescriptions.value(className).toBool(false)) { + shownDescriptions.insert(className, true); + params.put("ShownToggleDescriptions", QJsonDocument(shownDescriptions).toJson(QJsonDocument::Compact).toStdString()); + } } diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index e707cffd4..be202203e 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -2,13 +2,18 @@ #include "selfdrive/ui/qt/offroad/settings.h" -class DeveloperPanel : public ListWidget { +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + +class DeveloperPanel : public QFrame { Q_OBJECT public: explicit DeveloperPanel(SettingsWindow *parent); void showEvent(QShowEvent *event) override; // FrogPilot variables +signals: + void openSubPanel(); + void openSubSubPanel(); private: Params params; @@ -20,6 +25,40 @@ private: bool offroad = false; // FrogPilot variables + bool forceOpenDescriptions = false; + bool hasBSM = true; + bool hasOpenpilotLongitudinal = true; + bool hasRadar = true; + + int tuningLevel; + + std::map toggles; + + std::vector sidebarMetricsToggles; + + FrogPilotButtonsControl *sidebarMetricsToggle; + + FrogPilotButtonToggleControl *borderMetricsButton; + + FrogPilotManageControl *developerUIToggle; + + QJsonObject frogpilotToggleLevels; + + QSet developerMetricKeys = {"AdjacentPathMetrics", "BorderMetrics", "FPSCounter", "LeadInfo", "NumericalTemp", "SidebarMetrics", "UseSI"}; + QSet developerSidebarKeys = {"DeveloperSidebarMetric1", "DeveloperSidebarMetric2", "DeveloperSidebarMetric3", "DeveloperSidebarMetric4", "DeveloperSidebarMetric5", "DeveloperSidebarMetric6", "DeveloperSidebarMetric7"}; + QSet developerUIKeys = {"DeveloperMetrics", "DeveloperSidebar", "DeveloperWidgets"}; + QSet developerWidgetKeys = {"AdjacentLeadsUI", "RadarTracksUI", "ShowStoppingPoint"}; + + QSet parentKeys; + + QStackedLayout *mainLayout; + + QWidget *mainWidget; + + ScrollView *developerMetricPanel; + ScrollView *developerSidebarPanel; + ScrollView *developerUIPanel; + ScrollView *developerWidgetPanel; private slots: void updateToggles(bool _offroad); diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index e676456fb..63556cfde 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -16,6 +16,8 @@ #include "selfdrive/ui/qt/offroad/developer_panel.h" #include "selfdrive/ui/qt/offroad/firehose.h" +#include "frogpilot/ui/qt/offroad/frogpilot_settings.h" + TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { // param, title, desc, icon, restart needed std::vector> toggle_defs{ @@ -121,6 +123,9 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { toggles["ExperimentalMode"]->setConfirmation(true, true); // FrogPilot variables + connect(toggles["IsMetric"], &ToggleControl::toggleFlipped, [=](bool isMetric) { + updateMetric(isMetric); + }); } void TogglesPanel::updateState(const UIState &s) { @@ -414,6 +419,13 @@ void SettingsWindow::showEvent(QShowEvent *event) { // FrogPilot variables void SettingsWindow::hideEvent(QHideEvent *event) { + closePanel(); + closeSubPanel(); + + panelOpen = false; + subPanelOpen = false; + subSubPanelOpen = false; + subSubSubPanelOpen = false; } void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { @@ -466,7 +478,21 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { sidebar_layout->addWidget(close_btn, 0, Qt::AlignCenter); QObject::connect(close_btn, &QPushButton::clicked, [this]() { // FrogPilot variables - closeSettings(); + 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 @@ -482,14 +508,26 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); // FrogPilot variables + QObject::connect(toggles, &TogglesPanel::updateMetric, this, &SettingsWindow::updateMetric); + + FrogPilotSettingsWindow *frogpilotSettingsWindow = new FrogPilotSettingsWindow(this); + QObject::connect(frogpilotSettingsWindow, &FrogPilotSettingsWindow::openPanel, [this]() {panelOpen=true;}); + QObject::connect(frogpilotSettingsWindow, &FrogPilotSettingsWindow::openSubPanel, [this]() {subPanelOpen=true;}); + QObject::connect(frogpilotSettingsWindow, &FrogPilotSettingsWindow::openSubSubPanel, [this]() {subSubPanelOpen=true;}); + QObject::connect(frogpilotSettingsWindow, &FrogPilotSettingsWindow::openSubSubSubPanel, [this]() {subSubSubPanelOpen=true;}); + QObject::connect(frogpilotSettingsWindow, &FrogPilotSettingsWindow::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;}); QList> panels = { {tr("Device"), device}, {tr("Network"), networking}, {tr("Toggles"), toggles}, {tr("Software"), new SoftwarePanel(this)}, - {tr("Firehose"), new FirehosePanel(this)}, - {tr("Developer"), new DeveloperPanel(this)}, + {tr("Developer"), developerPanel}, + {tr("FrogPilot"), frogpilotSettingsWindow}, }; nav_btns = new QButtonGroup(this); @@ -524,7 +562,68 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { // FrogPilot variables + if (w->widget() == frogpilotSettingsWindow) { + bool tuningLevelConfirmed = params.getBool("TuningLevelConfirmed"); + if (!tuningLevelConfirmed) { + int frogpilotHours = QJsonDocument::fromJson(QString::fromStdString(params.get("FrogPilotStats")).toUtf8()).object().value("FrogPilotSeconds").toInt() / (60 * 60); + int openpilotHours = params.getInt("openpilotMinutes") / 60; + + if (frogpilotHours < 1 && openpilotHours < 100) { + if (openpilotHours < 10) { + if (ConfirmationDialog::alert(tr("Welcome to FrogPilot! 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 FrogPilot! Since you're new to FrogPilot, 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 (frogpilotHours < 50 && openpilotHours < 100) { + if (ConfirmationDialog::alert(tr("Since you're fairly new to FrogPilot, 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 (frogpilotHours < 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 FrogPilot, 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 (frogpilotHours >= 100) { + if (ConfirmationDialog::alert(tr("Since you're very experienced with FrogPilot, 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); }); @@ -551,4 +650,17 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { border-radius: 30px; } )"); + + // FrogPilot variables + updateDeveloperToggle(params.getInt("TuningLevel")); +} + +// FrogPilot variables +void SettingsWindow::updateDeveloperToggle(int tuningLevel) { + for (QAbstractButton *btn : nav_btns->buttons()) { + if (btn->text() == tr("Developer")) { + btn->setVisible(tuningLevel >= 3); + break; + } + } } diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index c5b7a5aa5..e0a1ca15a 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -36,6 +36,12 @@ signals: void scrollToToggle(const QString ¶m); // FrogPilot variables + void closePanel(); + void closeSubPanel(); + void closeSubSubPanel(); + void closeSubSubSubPanel(); + void updateMetric(bool isMetric, bool bootRun=false); + void updateTuningLevel(); private: QPushButton *sidebar_alert_widget; @@ -44,6 +50,13 @@ private: QStackedWidget *panel_widget; // FrogPilot variables + void updateDeveloperToggle(int tuningLevel); + + bool panelOpen; + bool subPanelOpen; + bool subSubPanelOpen; + bool subSubSubPanelOpen; + Params params; }; @@ -75,6 +88,7 @@ public: signals: // FrogPilot variables + void updateMetric(bool metric, bool bootRun=false); public slots: void expandToggleDescription(const QString ¶m); diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 40dda971f..992fe7407 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -49,7 +49,12 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons } if (!description->text().isEmpty()) { - description->setVisible(!description->isVisible()); + if (description->isVisible()) { + emit hideDescriptionEvent(); + description->setVisible(false); + } else { + description->setVisible(true); + } } }); diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index 4dc094a23..5ccf5ee22 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -68,6 +68,7 @@ signals: void showDescriptionEvent(); // FrogPilot variables + void hideDescriptionEvent(); protected: AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); @@ -135,6 +136,12 @@ public: } // FrogPilot variables + void forceOn(bool force) { + toggle.setEnabled(!force); + if (force && !toggle.on) { + toggle.togglePosition(); + } + } signals: void toggleFlipped(bool state); diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index cec7c4183..32c6fcefa 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -143,21 +143,27 @@ InputDialog::InputDialog(const QString &title, QWidget *parent, const QString &s line->backspace(); // FrogPilot variables + updateMaxLengthSublabel(line->text()); }); QObject::connect(k, &Keyboard::emitKey, this, [=](const QString &key) { - line->insert(key.left(1)); + if (line->text().length() < maxLength || maxLength == DEFAULT_MAX_LENGTH) { + line->insert(key.left(1)); + updateMaxLengthSublabel(line->text()); + } }); main_layout->addWidget(k, 2, Qt::AlignBottom); } QString InputDialog::getText(const QString &prompt, QWidget *parent, const QString &subtitle, - bool secret, int minLength, const QString &defaultText) { + bool secret, int minLength, const QString &defaultText, int maxLength) { InputDialog d(prompt, parent, subtitle, secret); d.line->setText(defaultText); d.setMinLength(minLength); // FrogPilot variables + d.setMaxLength(maxLength); + d.updateMaxLengthSublabel(defaultText); const int ret = d.exec(); return ret ? d.text() : QString(); @@ -192,11 +198,20 @@ void InputDialog::setMinLength(int length) { } // FrogPilot variables +void InputDialog::setMaxLength(int length) { + maxLength = length; +} + +void InputDialog::updateMaxLengthSublabel(const QString &text) { + if (maxLength != DEFAULT_MAX_LENGTH) { + sublabel->setText(tr("Characters: %1/%2").arg(text.length()).arg(maxLength)); + } +} // ConfirmationDialog ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, const QString &cancel_text, - const bool rich, QWidget *parent) : DialogBase(parent) { + const bool rich, QWidget *parent, const bool is_long) : DialogBase(parent) { QFrame *container = new QFrame(this); container->setStyleSheet(R"( QFrame { background-color: #1B1B1B; color: #C9C9C9; } @@ -204,7 +219,7 @@ ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString #confirm_btn:pressed { background-color: #3049F4; } )"); QVBoxLayout *main_layout = new QVBoxLayout(container); - main_layout->setContentsMargins(32, rich ? 32 : 120, 32, 32); + main_layout->setContentsMargins(32, rich || is_long ? 32 : 120, 32, 32); QLabel *prompt = new QLabel(prompt_text, this); prompt->setWordWrap(true); @@ -236,8 +251,8 @@ ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString outer_layout->addWidget(container); } -bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent) { - ConfirmationDialog d(prompt_text, tr("Ok"), "", false, parent); +bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent, bool is_long) { + ConfirmationDialog d(prompt_text, tr("Ok"), "", false, parent, is_long); return d.exec(); } diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h index 2cbbe21ac..700e114f1 100644 --- a/selfdrive/ui/qt/widgets/input.h +++ b/selfdrive/ui/qt/widgets/input.h @@ -9,6 +9,7 @@ #include "selfdrive/ui/qt/widgets/keyboard.h" +const int DEFAULT_MAX_LENGTH = 512; class DialogBase : public QDialog { Q_OBJECT @@ -27,13 +28,14 @@ class InputDialog : public DialogBase { public: explicit InputDialog(const QString &title, QWidget *parent, const QString &subtitle = "", bool secret = false); static QString getText(const QString &title, QWidget *parent, const QString &subtitle = "", - bool secret = false, int minLength = -1, const QString &defaultText = ""); + bool secret = false, int minLength = -1, const QString &defaultText = "", int maxLength = DEFAULT_MAX_LENGTH); QString text(); void setMessage(const QString &message, bool clearInputField = true); void setMinLength(int length); void show(); // FrogPilot variables + void setMaxLength(int length); private: int minLength; @@ -45,6 +47,9 @@ private: QPushButton *eye_btn; // FrogPilot variables + void updateMaxLengthSublabel(const QString &text); + + int maxLength; private slots: void handleEnter(); @@ -59,8 +64,8 @@ class ConfirmationDialog : public DialogBase { public: explicit ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, - const QString &cancel_text, const bool rich, QWidget* parent); - static bool alert(const QString &prompt_text, QWidget *parent); + const QString &cancel_text, const bool rich, QWidget* parent, const bool is_long=false); + static bool alert(const QString &prompt_text, QWidget *parent, bool isLong=false); static bool confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent); static bool rich(const QString &prompt_text, QWidget *parent); }; diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 4109a3b6e..d3c1353fb 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -150,6 +150,10 @@ void UIState::update() { FrogPilotUIState *fs = frogpilotUIState(); FrogPilotUIScene &frogpilot_scene = fs->frogpilot_scene; + if (frogpilot_scene.frogpilot_panel_active) { + device()->resetInteractiveTimeout(); + } + fs->update(); } diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index 65880bdad..d8a235ad3 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -6,6 +6,7 @@ import os from openpilot.common.basedir import BASEDIR UI_DIR = os.path.join(BASEDIR, "selfdrive", "ui") +FROGPILOT_UI_DIR = os.path.join(BASEDIR, "frogpilot", "ui") TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations") LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") TRANSLATIONS_INCLUDE_FILE = os.path.join(TRANSLATIONS_DIR, "alerts_generated.h") @@ -31,7 +32,7 @@ def update_translations(vanish: bool = False, translation_files: None | list[str for file in translation_files: tr_file = os.path.join(translations_dir, f"{file}.ts") - args = f"lupdate -locations none -recursive {UI_DIR} -ts {tr_file} -I {BASEDIR}" + args = f"lupdate -locations none -recursive {UI_DIR} {FROGPILOT_UI_DIR} -ts {tr_file} -I {BASEDIR}" if vanish: args += " -no-obsolete" if file in PLURAL_ONLY: