#include "selfdrive/ui/qt/home.h" #include #include #include #include #include #include "common/swaglog.h" #include "common/params.h" #include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/input.h" #include "selfdrive/ui/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/network/networking.h" #include "cereal/messaging/messaging.h" // HomeWindow: the container for the offroad and onroad UIs HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { // CLEARPILOT Sidebar set to invisible in drive view. params.putBool("Sidebar", false); QHBoxLayout *main_layout = new QHBoxLayout(this); main_layout->setMargin(0); main_layout->setSpacing(0); sidebar = new Sidebar(this); sidebar->setVisible(false); main_layout->addWidget(sidebar); QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings); QObject::connect(sidebar, &Sidebar::openOnroad, this, &HomeWindow::showOnroad); slayout = new QStackedLayout(); main_layout->addLayout(slayout); home = new ClearPilotPanel(this); QObject::connect(home, &ClearPilotPanel::openSettings, this, &HomeWindow::openSettings); QObject::connect(home, &ClearPilotPanel::openStatus, this, &HomeWindow::openStatus); QObject::connect(home, &ClearPilotPanel::closePanel, [=]() { // Return to splash or onroad depending on state if (uiState()->scene.started) { slayout->setCurrentWidget(onroad); } else { slayout->setCurrentWidget(ready); } }); slayout->addWidget(home); onroad = new OnroadWindow(this); slayout->addWidget(onroad); // CLEARPILOT ready = new ReadyWindow(this); slayout->addWidget(ready); slayout->setCurrentWidget(ready); // show splash immediately, not ClearPilotPanel driver_view = new DriverViewWindow(this); connect(driver_view, &DriverViewWindow::done, [=] { showDriverView(false); }); slayout->addWidget(driver_view); setAttribute(Qt::WA_NoSystemBackground); QObject::connect(uiState(), &UIState::uiUpdate, this, &HomeWindow::updateState); QObject::connect(uiState(), &UIState::offroadTransition, this, &HomeWindow::offroadTransition); QObject::connect(uiState(), &UIState::offroadTransition, sidebar, &Sidebar::offroadTransition); } // Debug function to activate onroad UI void HomeWindow::showOnroad() { sidebar->setVisible(false); slayout->setCurrentWidget(onroad); // sidebar->setVisible(params.getBool("Sidebar")); } void HomeWindow::showSidebar(bool show) { sidebar->setVisible(show); } void HomeWindow::updateState(const UIState &s) { if (s.scene.started) { showDriverView(s.scene.driver_camera_timer >= 10, true); // CLEARPILOT: show splash screen when onroad but in park bool parked = s.scene.parked; int screenMode = paramsMemory.getInt("ScreenDisplayMode"); bool nightrider = (screenMode == 1 || screenMode == 4); if (parked && !was_parked_onroad) { LOGW("CLP UI: park transition -> showing splash"); slayout->setCurrentWidget(ready); // If we were in nightrider mode, switch to screen off if (nightrider) { paramsMemory.putInt("ScreenDisplayMode", 3); } } else if (!parked && was_parked_onroad) { LOGW("CLP UI: drive transition -> showing onroad"); slayout->setCurrentWidget(onroad); ready->has_driven = true; } was_parked_onroad = parked; // CLEARPILOT: honor display on/off while showing splash in park (normal mode only) if (parked && ready->isVisible()) { if (screenMode == 3) { Hardware::set_display_power(false); } else { Hardware::set_display_power(true); } } } } void HomeWindow::offroadTransition(bool offroad) { sidebar->setVisible(false); if (offroad) { LOGW("CLP UI: offroad transition -> showing splash"); was_parked_onroad = false; slayout->setCurrentWidget(ready); } else { // CLEARPILOT: start onroad in splash — updateState will switch to // camera view once the car shifts out of park LOGW("CLP UI: onroad transition -> showing splash (parked)"); was_parked_onroad = true; slayout->setCurrentWidget(ready); } } void HomeWindow::showDriverView(bool show, bool started) { if (show) { LOGW("CLP UI: showDriverView(true) -> driver_view"); emit closeSettings(); slayout->setCurrentWidget(driver_view); sidebar->setVisible(false); } else if (!started) { // Offroad, not started — show home menu slayout->setCurrentWidget(home); sidebar->setVisible(false); } // CLEARPILOT: when started, don't touch slayout here — // updateState handles park->splash and drive->onroad transitions } void HomeWindow::mousePressEvent(QMouseEvent* e) { // CLEARPILOT: tap from any view goes to ClearPilotPanel if (ready->isVisible() || onroad->isVisible()) { LOGW("CLP UI: tap -> showing ClearPilotPanel"); sidebar->setVisible(false); home->resetToGeneral(); slayout->setCurrentWidget(home); } } void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { HomeWindow::mousePressEvent(e); // const SubMaster &sm = *(uiState()->sm); } // CLEARPILOT: ClearPilotPanel — settings-style sidebar menu static const char *clpSidebarBtnStyle = R"( QPushButton { color: grey; border: none; background: none; font-size: 65px; font-weight: 500; } QPushButton:checked { color: white; } QPushButton:pressed { color: #ADADAD; } )"; // clpActionBtnStyle removed — no longer used // Shutdown timer: param value -> display label static QString shutdownLabel(int val) { if (val == 0) return "5 mins"; if (val <= 3) return QString::number(val * 15) + " mins"; int hours = val - 3; return QString::number(hours) + (hours == 1 ? " hour" : " hours"); } ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) { // Sidebar QWidget *sidebar_widget = new QWidget; QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); sidebar_layout->setContentsMargins(50, 50, 100, 50); // Close button QPushButton *close_btn = new QPushButton("← Back"); close_btn->setStyleSheet(R"( QPushButton { color: white; border-radius: 25px; background: #292929; font-size: 50px; font-weight: 500; } QPushButton:pressed { color: #ADADAD; } )"); close_btn->setFixedSize(300, 125); sidebar_layout->addSpacing(10); sidebar_layout->addWidget(close_btn, 0, Qt::AlignRight); QObject::connect(close_btn, &QPushButton::clicked, [=]() { emit closePanel(); }); // Panel content area panel_widget = new QStackedWidget(); // ── General panel ── ListWidget *general_panel = new ListWidget(this); general_panel->setContentsMargins(50, 25, 50, 25); // Status button auto *status_btn = new ButtonControl("System Status", "VIEW", ""); connect(status_btn, &ButtonControl::clicked, [=]() { emit openStatus(); }); general_panel->addItem(status_btn); // Reset Calibration auto resetCalibBtn = new ButtonControl("Reset Calibration", "RESET", ""); connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, [=]() { QString desc = "openpilot requires the device to be mounted within 4° left or right and " "within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required."; std::string calib_bytes = Params().get("CalibrationParams"); if (!calib_bytes.empty()) { try { AlignedBuffer aligned_buf; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size())); auto calib = cmsg.getRoot().getLiveCalibration(); if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) { double pitch = calib.getRpyCalib()[1] * (180 / M_PI); double yaw = calib.getRpyCalib()[2] * (180 / M_PI); desc += QString(" Your device is pointed %1° %2 and %3° %4.") .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? "down" : "up", QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? "left" : "right"); } } catch (...) { qInfo() << "invalid CalibrationParams"; } } qobject_cast(sender())->setDescription(desc); }); connect(resetCalibBtn, &ButtonControl::clicked, [=]() { if (ConfirmationDialog::confirm("Are you sure you want to reset calibration?", "Reset", this)) { Params().remove("CalibrationParams"); Params().remove("LiveTorqueParameters"); } }); general_panel->addItem(resetCalibBtn); // Shutdown Timer int cur_shutdown = Params().getInt("DeviceShutdown"); auto shutdownBtn = new ButtonControl("Shutdown Timer", shutdownLabel(cur_shutdown), "How long the device stays on after the car is turned off."); connect(shutdownBtn, &ButtonControl::clicked, [=]() { QStringList options; for (int i = 0; i <= 33; i++) { options << shutdownLabel(i); } int current = Params().getInt("DeviceShutdown"); QString sel = MultiOptionDialog::getSelection("Shutdown Timer", options, shutdownLabel(current), this); if (!sel.isEmpty()) { int idx = options.indexOf(sel); if (idx >= 0) { Params().putInt("DeviceShutdown", idx); shutdownBtn->setValue(shutdownLabel(idx)); } } }); general_panel->addItem(shutdownBtn); // Power buttons QHBoxLayout *power_layout = new QHBoxLayout(); power_layout->setSpacing(30); QPushButton *reboot_btn = new QPushButton("Reboot"); reboot_btn->setObjectName("reboot_btn"); power_layout->addWidget(reboot_btn); QPushButton *softreboot_btn = new QPushButton("Soft Reboot"); softreboot_btn->setObjectName("softreboot_btn"); power_layout->addWidget(softreboot_btn); QPushButton *poweroff_btn = new QPushButton("Power Off"); poweroff_btn->setObjectName("poweroff_btn"); power_layout->addWidget(poweroff_btn); QObject::connect(reboot_btn, &QPushButton::clicked, [=]() { if (!uiState()->engaged()) { if (ConfirmationDialog::confirm("Are you sure you want to reboot?", "Reboot", this)) { if (!uiState()->engaged()) { Params().putBool("DoReboot", true); } } } else { ConfirmationDialog::alert("Disengage to Reboot", this); } }); QObject::connect(softreboot_btn, &QPushButton::clicked, [=]() { if (!uiState()->engaged()) { if (ConfirmationDialog::confirm("Are you sure you want to soft reboot?", "Soft Reboot", this)) { if (!uiState()->engaged()) { Params().putBool("DoSoftReboot", true); } } } else { ConfirmationDialog::alert("Disengage to Soft Reboot", this); } }); QObject::connect(poweroff_btn, &QPushButton::clicked, [=]() { if (!uiState()->engaged()) { if (ConfirmationDialog::confirm("Are you sure you want to power off?", "Power Off", this)) { if (!uiState()->engaged()) { Params().putBool("DoShutdown", true); } } } else { ConfirmationDialog::alert("Disengage to Power Off", this); } }); general_panel->addItem(power_layout); // ── Network panel ── Networking *network_panel = new Networking(this); // Hide APN button — find by searching QPushButton labels inside AdvancedNetworking for (auto *btn : network_panel->findChildren()) { if (btn->text().contains("APN", Qt::CaseInsensitive)) { // Hide the parent AbstractControl frame, not just the button if (auto *frame = qobject_cast(btn->parentWidget())) { frame->setVisible(false); } } } // ── Dashcam panel ── QWidget *dashcam_panel = new QWidget(this); QVBoxLayout *dash_layout = new QVBoxLayout(dashcam_panel); dash_layout->setContentsMargins(50, 25, 50, 25); QLabel *dash_label = new QLabel("Dashcam viewer coming soon"); dash_label->setStyleSheet("color: grey; font-size: 40px;"); dash_label->setAlignment(Qt::AlignCenter); dash_layout->addWidget(dash_label); dash_layout->addStretch(); // ── Debug panel ── ListWidget *debug_panel = new ListWidget(this); debug_panel->setContentsMargins(50, 25, 50, 25); auto *telemetry_toggle = new ToggleControl("Telemetry Logging", "Record telemetry data to CSV in the session log directory. " "Captures only changed values for efficiency.", "", Params("/dev/shm/params").getBool("TelemetryEnabled"), this); QObject::connect(telemetry_toggle, &ToggleControl::toggleFlipped, [](bool on) { Params("/dev/shm/params").putBool("TelemetryEnabled", on); }); debug_panel->addItem(telemetry_toggle); auto *vpn_toggle = new ToggleControl("VPN", "Connect to vpn.hanson.xyz for remote SSH access. " "Disabling kills the active tunnel and stops reconnection attempts.", "", Params("/dev/shm/params").getBool("VpnEnabled"), this); QObject::connect(vpn_toggle, &ToggleControl::toggleFlipped, [](bool on) { Params("/dev/shm/params").putBool("VpnEnabled", on); if (on) { std::system("sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'"); } else { std::system("sudo pkill -TERM -f vpn-monitor.sh; sudo killall openvpn"); } }); debug_panel->addItem(vpn_toggle); // ── Register panels with sidebar buttons ── QList> panels = { {"General", general_panel}, {"Network", network_panel}, {"Dashcam", dashcam_panel}, {"Debug", debug_panel}, }; nav_group = new QButtonGroup(this); nav_group->setExclusive(true); for (auto &[name, panel] : panels) { QPushButton *btn = new QPushButton(name); btn->setCheckable(true); btn->setStyleSheet(clpSidebarBtnStyle); btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); sidebar_layout->addWidget(btn, 0, Qt::AlignRight); nav_group->addButton(btn); // Network panel handles its own scrolling/margins if (name == "Network") { panel_widget->addWidget(panel); QObject::connect(btn, &QPushButton::clicked, [=, w = panel]() { panel_widget->setCurrentWidget(w); }); } else { ScrollView *panel_frame = new ScrollView(panel, this); panel_widget->addWidget(panel_frame); QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { panel_widget->setCurrentWidget(w); }); } } // Select General by default nav_group->buttons().first()->setChecked(true); panel_widget->setCurrentIndex(0); // Main layout: sidebar + panels QHBoxLayout *main_layout = new QHBoxLayout(this); sidebar_widget->setFixedWidth(500); main_layout->addWidget(sidebar_widget); main_layout->addWidget(panel_widget); setStyleSheet(R"( * { color: white; font-size: 50px; } ClearPilotPanel { background-color: black; } QStackedWidget, ScrollView, Networking { background-color: #292929; border-radius: 30px; } #softreboot_btn { height: 120px; border-radius: 15px; background-color: #e2e22c; } #softreboot_btn:pressed { background-color: #ffe224; } #reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; } #reboot_btn:pressed { background-color: #4a4a4a; } #poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; } #poweroff_btn:pressed { background-color: #FF2424; } )"); } void ClearPilotPanel::resetToGeneral() { panel_widget->setCurrentIndex(0); nav_group->buttons().first()->setChecked(true); }