diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 2a4efc5..e35d287 100755 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -39,7 +39,7 @@ qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc", "qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc", "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", - "../frogpilot/screenrecorder/omx_encoder.cc", "../frogpilot/screenrecorder/screenrecorder.cc", + "../frogpilot/screenrecorder/omx_encoder.cc", # kept for dashcamd .o dependency "qt/ready.cc"] # build translation files @@ -77,8 +77,7 @@ qt_env.Program("_text", ["qt/text.cc"], LIBS=qt_libs) qt_env.Program("_spinner", ["qt/spinner.cc"], LIBS=qt_libs) -# Clearpilot -# Create clearpilot tools +# Clearpilot tools qt_env.Program("/data/openpilot/system/clearpilot/tools/qt_shell", ["/data/openpilot/system/clearpilot/tools/qt_shell.cc"], LIBS=qt_libs) # build main UI diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index 4903a3d..1086a56 100755 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -1,4 +1,9 @@ #include +#include +#include +#include +#include +#include #include #include @@ -8,7 +13,27 @@ #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/window.h" +// CLEARPILOT: crash handler — prints stack trace to stderr on SIGSEGV/SIGABRT +static void crash_handler(int sig) { + const char *sig_name = (sig == SIGSEGV) ? "SIGSEGV" : (sig == SIGABRT) ? "SIGABRT" : "SIGNAL"; + fprintf(stderr, "\n=== CRASH: %s (signal %d) ===\n", sig_name, sig); + + void *frames[64]; + int count = backtrace(frames, 64); + fprintf(stderr, "Backtrace (%d frames):\n", count); + backtrace_symbols_fd(frames, count, STDERR_FILENO); + fprintf(stderr, "=== END CRASH ===\n"); + fflush(stderr); + + // Re-raise to get default behavior (core dump / exit) + signal(sig, SIG_DFL); + raise(sig); +} + int main(int argc, char *argv[]) { + signal(SIGSEGV, crash_handler); + signal(SIGABRT, crash_handler); + setpriority(PRIO_PROCESS, 0, -20); qInstallMessageHandler(swagLogMessageHandler); diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index 0b9d0d0..f758097 100755 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -1,14 +1,20 @@ #include "selfdrive/ui/qt/home.h" +#include #include #include #include #include -#include "selfdrive/ui/qt/offroad/experimental_mode.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/drive_stats.h" +#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 @@ -21,6 +27,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { 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); @@ -28,8 +35,17 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { slayout = new QStackedLayout(); main_layout->addLayout(slayout); - home = new OffroadHome(this); - QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings); + 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); @@ -38,7 +54,8 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { // 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); @@ -64,52 +81,79 @@ void HomeWindow::showSidebar(bool show) { } void HomeWindow::updateState(const UIState &s) { - // const SubMaster &sm = *(s.sm); 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) { - sidebar->setVisible(false); + LOGW("CLP UI: offroad transition -> showing splash"); + was_parked_onroad = false; slayout->setCurrentWidget(ready); } else { - sidebar->setVisible(false); - slayout->setCurrentWidget(onroad); + // CLEARPILOT: start onroad in splash — updateState will switch to + // camera view once the car shifts out of park. Reset has_driven so + // fresh ignition shows the READY text (not the post-drive textless splash). + LOGW("CLP UI: onroad transition -> showing splash (parked)"); + was_parked_onroad = true; + ready->has_driven = false; + 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(show == false); - } else { - if (started) { - slayout->setCurrentWidget(onroad); - sidebar->setVisible(params.getBool("Sidebar")); - } else { - slayout->setCurrentWidget(home); - sidebar->setVisible(show == false); - } + 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 todo - tap on main goes straight to settings - // Unless we click a debug widget. - - // CLEARPILOT - click ready shows home - if (!onroad->isVisible() && ready->isVisible()) { - sidebar->setVisible(true); + // 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); } - - // Todo: widgets - if (onroad->isVisible()) { - emit openSettings(); - } } void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { @@ -117,132 +161,274 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { // const SubMaster &sm = *(uiState()->sm); } -// OffroadHome: the offroad home page +// CLEARPILOT: ClearPilotPanel — settings-style sidebar menu -OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { - QVBoxLayout* main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(40, 40, 40, 40); - - // top header - QHBoxLayout* header_layout = new QHBoxLayout(); - header_layout->setContentsMargins(0, 0, 0, 0); - header_layout->setSpacing(16); - - update_notif = new QPushButton(tr("UPDATE")); - update_notif->setVisible(false); - update_notif->setStyleSheet("background-color: #364DEF;"); - QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); }); - header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); - - alert_notif = new QPushButton(); - alert_notif->setVisible(false); - alert_notif->setStyleSheet("background-color: #E22C2C;"); - QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); }); - header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); - - date = new ElidedLabel(); - header_layout->addWidget(date, 0, Qt::AlignHCenter | Qt::AlignLeft); - - version = new ElidedLabel(); - header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight); - - main_layout->addLayout(header_layout); - - // main content - main_layout->addSpacing(25); - center_layout = new QStackedLayout(); - - QWidget *home_widget = new QWidget(this); - { - QHBoxLayout *home_layout = new QHBoxLayout(home_widget); - home_layout->setContentsMargins(0, 0, 0, 0); - home_layout->setSpacing(30); - - // // // Create a QWebEngineView - // QWebEngineView *web_view = new QWebEngineView(); - // web_view->load(QUrl("http://fark.com")); - - // // Add the QWebEngineView to the layout - // home_layout->addWidget(web_view); +static const char *clpSidebarBtnStyle = R"( + QPushButton { + color: grey; + border: none; + background: none; + font-size: 65px; + font-weight: 500; } - center_layout->addWidget(home_widget); + QPushButton:checked { + color: white; + } + QPushButton:pressed { + color: #ADADAD; + } +)"; - // add update & alerts widgets - update_widget = new UpdateAlert(); - QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); - center_layout->addWidget(update_widget); - alerts_widget = new OffroadAlert(); - QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); - center_layout->addWidget(alerts_widget); +// clpActionBtnStyle removed — no longer used - main_layout->addLayout(center_layout, 1); - // set up refresh timer - timer = new QTimer(this); - timer->callOnTimeout(this, &OffroadHome::refresh); +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); + + // 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 *health_metrics_toggle = new ToggleControl("System Health Overlay", + "Show controls lag, model frame drops, temperature, CPU, and memory usage " + "in the lower-right of the onroad UI. For diagnosing slowdown issues.", "", + Params().getBool("ClearpilotShowHealthMetrics"), this); + QObject::connect(health_metrics_toggle, &ToggleControl::toggleFlipped, [](bool on) { + Params().putBool("ClearpilotShowHealthMetrics", on); + }); + debug_panel->addItem(health_metrics_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; } - OffroadHome { + ClearPilotPanel { background-color: black; } - OffroadHome > QPushButton { - padding: 15px 30px; - border-radius: 5px; - font-size: 40px; - font-weight: 500; - } - OffroadHome > QLabel { - font-size: 55px; + 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; } )"); } -/* Refresh data on screen every 5 seconds. */ -void OffroadHome::showEvent(QShowEvent *event) { - refresh(); - timer->start(5 * 1000); -} - -void OffroadHome::hideEvent(QHideEvent *event) { - timer->stop(); -} - -void OffroadHome::refresh() { - QString model = QString::fromStdString(params.get("ModelName")); - - date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d")); - version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + model); - - // bool updateAvailable = update_widget->refresh(); - - int alerts = alerts_widget->refresh(); - - if (alerts > 0 && !alerts_widget->isVisible()) { - alerts_widget->setVisible(true); - } else if (alerts == 0 && alerts_widget->isVisible()) { - alerts_widget->setVisible(false); - } - - // pop-up new notification - // CLEARPILOT temp disabled update notifications - // int idx = center_layout->currentIndex(); - // if (!updateAvailable && !alerts && false) { - // idx = 0; - // } else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) { - // idx = 1; - // } else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) { - // idx = 2; - // } - // center_layout->setCurrentIndex(idx); - - // CLEARPILOT temp disabled update notifications -// update_notif->setVisible(updateAvailable); -// alert_notif->setVisible(alerts); - alert_notif->setVisible(false); - if (alerts) { - alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT"))); - } +void ClearPilotPanel::resetToGeneral() { + panel_widget->setCurrentIndex(0); + nav_group->buttons().first()->setChecked(true); } diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 5e71b16..bec2527 100755 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include #include +#include #include #include @@ -16,32 +18,23 @@ #include "selfdrive/ui/qt/widgets/offroad_alerts.h" #include "selfdrive/ui/ui.h" -class OffroadHome : public QFrame { +class ClearPilotPanel : public QFrame { Q_OBJECT public: - explicit OffroadHome(QWidget* parent = 0); + explicit ClearPilotPanel(QWidget* parent = 0); signals: void openSettings(int index = 0, const QString ¶m = ""); + void openStatus(); + void closePanel(); + +public: + void resetToGeneral(); private: - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - void refresh(); - - Params params; - - QTimer* timer; - ElidedLabel* version; - QStackedLayout* center_layout; - UpdateAlert *update_widget; - OffroadAlert* alerts_widget; - QPushButton* alert_notif; - QPushButton* update_notif; - - // FrogPilot variables - ElidedLabel* date; + QStackedWidget *panel_widget; + QButtonGroup *nav_group; }; class HomeWindow : public QWidget { @@ -53,6 +46,7 @@ public: signals: void openSettings(int index = 0, const QString ¶m = ""); + void openStatus(); void closeSettings(); public slots: @@ -67,17 +61,18 @@ protected: private: Sidebar *sidebar; - OffroadHome *home; + ClearPilotPanel *home; OnroadWindow *onroad; DriverViewWindow *driver_view; QStackedLayout *slayout; // FrogPilot variables Params params; + Params paramsMemory{"/dev/shm/params"}; // CLEARPILOT - // bool show_ready; ReadyWindow *ready; + bool was_parked_onroad = false; private slots: void updateState(const UIState &s); diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index d2b3f0c..558d02e 100755 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -75,12 +76,13 @@ void OnroadWindow::updateState(const UIState &s) { alerts->updateAlert(alert); nvg->updateState(s); + nvg->update(); // CLEARPILOT: force repaint every frame for HUD elements QColor bgColor = bg_colors[s.status]; - - if (paramsMemory.getBool("no_lat_lane_change") == 1 || nvg->screenDisplayMode == 2) { + // CLEARPILOT: read from paramsMemory; controlsd writes "no_lat_lane_change". + if (paramsMemory.getBool("no_lat_lane_change")) { bgColor = bg_colors[STATUS_DISENGAGED]; - } + } if (bg != bgColor) { bg = bgColor; @@ -176,7 +178,14 @@ void OnroadWindow::offroadTransition(bool offroad) { void OnroadWindow::paintEvent(QPaintEvent *event) { QPainter p(this); - p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255)); + // CLEARPILOT: hide engagement border in nightrider mode + int dm = paramsMemory.getInt("ScreenDisplayMode"); + bool nightrider = (dm == 1 || dm == 4); + if (nightrider) { + p.fillRect(rect(), Qt::black); + } else { + p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255)); + } QString logicsDisplayString = QString(); if (scene.show_jerk) { @@ -216,6 +225,13 @@ void OnroadWindow::paintEvent(QPaintEvent *event) { } } +// void OnroadWindow::update_screen_on_off() { +// int screenDisaplayMode = paramsMemory.getInt("ScreenDisaplayMode"); +// if (screenDisaplayMode == 1) { +// // Conditionally off +// } +// } + // ***** onroad widgets ***** @@ -308,10 +324,7 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par QHBoxLayout *buttons_layout = new QHBoxLayout(); buttons_layout->setSpacing(0); - // Neokii screen recorder - recorder_btn = new ScreenRecorder(this); - recorder_btn->setVisible(false); - // buttons_layout->addWidget(recorder_btn); + // Neokii screen recorder — DISABLED: using dashcamd instead QVBoxLayout *top_right_layout = new QVBoxLayout(); top_right_layout->setSpacing(0); @@ -327,24 +340,13 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par } void AnnotatedCameraWidget::updateState(const UIState &s) { - - screenDisplayMode = paramsMemory.getInt("ScreenDisplayMode"); - if (screenDisplayMode == 2 && !alert_is_visible) { - // Draw black, filled, full-size rectangle to blank the screen - // p.fillRect(0, 0, width(), height(), Qt::black); - // p.restore(); - Hardware::set_display_power(false); - return; - } else { - Hardware::set_display_power(true); - } - const int SET_SPEED_NA = 255; const SubMaster &sm = *(s.sm); const bool cs_alive = sm.alive("controlsState"); const auto cs = sm["controlsState"].getControlsState(); const auto car_state = sm["carState"].getCarState(); + (void)car_state; // CLEARPILOT: suppress unused warning, will use later // Handle older routes where vCruiseCluster is not set float v_cruise = cs.getVCruiseCluster() == 0.0 ? cs.getVCruise() : cs.getVCruiseCluster(); @@ -354,11 +356,16 @@ void AnnotatedCameraWidget::updateState(const UIState &s) { setSpeed *= KM_TO_MILE; } - // Handle older routes where vEgoCluster is not set - v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0; - float v_ego = v_ego_cluster_seen && !scene.wheel_speed ? car_state.getVEgoCluster() : car_state.getVEgo(); - speed = cs_alive ? std::max(0.0, v_ego) : 0.0; - speed *= s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH; + // CLEARPILOT: read speed and speed limit from params (written by speed_logic.py ~2Hz) + clpParamFrame++; + if (clpParamFrame % 10 == 0) { // ~2Hz at 20Hz UI update rate + clpHasSpeed = paramsMemory.get("ClearpilotHasSpeed") == "1"; + clpSpeedDisplay = QString::fromStdString(paramsMemory.get("ClearpilotSpeedDisplay")); + clpSpeedLimitDisplay = QString::fromStdString(paramsMemory.get("ClearpilotSpeedLimitDisplay")); + clpSpeedUnit = QString::fromStdString(paramsMemory.get("ClearpilotSpeedUnit")); + clpCruiseWarning = QString::fromStdString(paramsMemory.get("ClearpilotCruiseWarning")); + clpCruiseWarningSpeed = QString::fromStdString(paramsMemory.get("ClearpilotCruiseWarningSpeed")); + } // auto speed_limit_sign = nav_instruction.getSpeedLimitSign(); // speedLimit = slcOverridden ? scene.speed_limit_overridden_speed : speedLimitController ? scene.speed_limit : nav_alive ? nav_instruction.getSpeedLimit() : 0.0; @@ -394,9 +401,28 @@ if (edgeColor != bgColor) { } void AnnotatedCameraWidget::drawHud(QPainter &p) { - // Blank when screenDisplayMode=1 + // CLEARPILOT: display power control based on ScreenDisplayMode p.save(); - + + if (displayMode == 3 && !alert_is_visible) { + Hardware::set_display_power(false); + p.restore(); + return; + } else { + Hardware::set_display_power(true); + } + + // CLEARPILOT: blinking blue circle when telemetry is recording + if (paramsMemory.getBool("TelemetryEnabled")) { + // Blink: visible for 500ms, hidden for 500ms + int phase = (QDateTime::currentMSecsSinceEpoch() / 500) % 2; + if (phase == 0) { + p.setPen(Qt::NoPen); + p.setBrush(QColor(30, 100, 220)); + p.drawEllipse(width() - 150, 50, 100, 100); + } + } + // Header gradient QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT); bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45)); @@ -407,7 +433,7 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) { // QString speedLimitStr = (speedLimit > 1) ? QString::number(std::nearbyint(speedLimit)) : "–"; // QString speedLimitOffsetStr = slcSpeedLimitOffset == 0 ? "–" : QString::number(slcSpeedLimitOffset, 'f', 0).prepend(slcSpeedLimitOffset > 0 ? "+" : ""); - QString speedStr = QString::number(std::nearbyint(speed)); + // QString speedStr = QString::number(std::nearbyint(speed)); QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed - cruiseAdjustment)) : "–"; p.restore(); @@ -431,17 +457,21 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) { // Todo: lead speed // Todo: Experimental speed - // // current speed - if (!(scene.hide_speed)) { - // CLEARPILOT changes to 120 from ~176 - // Maybe we want to hide this? + // CLEARPILOT: show speed from speed_logic params, hide when no speed or speed=0 + if (clpHasSpeed && !clpSpeedDisplay.isEmpty() && !scene.hide_speed) { p.setFont(InterFont(140, QFont::Bold)); - drawText(p, rect().center().x(), 210, speedStr); - // CLEARPILOT changes to 40 from 66 + drawText(p, rect().center().x(), 210, clpSpeedDisplay); p.setFont(InterFont(50)); - drawText(p, rect().center().x(), 290, speedUnit, 200); + drawText(p, rect().center().x(), 290, clpSpeedUnit, 200); } - + + // CLEARPILOT: speed limit sign in lower-left, cruise warning above it + drawSpeedLimitSign(p); + drawCruiseWarningSign(p); + + // CLEARPILOT: system health metrics in lower-right (debug overlay) + drawHealthMetrics(p); + // Draw FrogPilot widgets paintFrogPilotWidgets(p); } @@ -535,6 +565,164 @@ void AnnotatedCameraWidget::drawSpeedWidget(QPainter &p, int x, int y, const QSt } +void AnnotatedCameraWidget::drawSpeedLimitSign(QPainter &p) { + // Hide when no speed limit or speed limit is 0 + if (clpSpeedLimitDisplay.isEmpty() || clpSpeedLimitDisplay == "0") return; + + p.save(); + + const int signW = 189; + const int signH = 239; + const int margin = 20; + const int borderW = 6; + const int innerBorderW = 4; + const int innerMargin = 10; + const int cornerR = 15; + + // Position: 20px from lower-left corner + QRect signRect(margin, height() - signH - margin, signW, signH); + + if (nightriderMode) { + // Nightrider: black background, light gray-blue border and text + QColor borderColor(160, 180, 210); + QColor textColor(160, 180, 210); + + // Outer border + p.setPen(QPen(borderColor, borderW)); + p.setBrush(QColor(0, 0, 0, 220)); + p.drawRoundedRect(signRect, cornerR, cornerR); + + // Inner border + QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin); + p.setPen(QPen(borderColor, innerBorderW)); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4); + + // "SPEED" text + p.setPen(textColor); + p.setFont(InterFont(30, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SPEED"); + + // "LIMIT" text + p.setFont(InterFont(30, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 48, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "LIMIT"); + + // Speed limit number — shifted down ~10% of innerRect height via extra top inset + p.setFont(InterFont(90, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay); + } else { + // Normal: white background, black border and text + QColor borderColor(0, 0, 0); + QColor textColor(0, 0, 0); + + // Outer border + p.setPen(QPen(borderColor, borderW)); + p.setBrush(QColor(255, 255, 255, 240)); + p.drawRoundedRect(signRect, cornerR, cornerR); + + // Inner border + QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin); + p.setPen(QPen(borderColor, innerBorderW)); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4); + + // "SPEED" text + p.setPen(textColor); + p.setFont(InterFont(30, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SPEED"); + + // "LIMIT" text + p.setFont(InterFont(30, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 48, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "LIMIT"); + + // Speed limit number — shifted down ~10% of innerRect height via extra top inset + p.setFont(InterFont(90, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpSpeedLimitDisplay); + } + + p.restore(); +} + +void AnnotatedCameraWidget::drawCruiseWarningSign(QPainter &p) { + // Only show when there's an active warning and the speed limit sign is visible + if (clpCruiseWarning.isEmpty() || clpCruiseWarningSpeed.isEmpty()) return; + if (clpSpeedLimitDisplay.isEmpty() || clpSpeedLimitDisplay == "0") return; + + bool isOver = (clpCruiseWarning == "over"); + if (!isOver && clpCruiseWarning != "under") return; + + p.save(); + + // Same dimensions as speed limit sign + const int signW = 189; + const int signH = 239; + const int margin = 20; + const int borderW = 6; + const int innerBorderW = 4; + const int innerMargin = 10; + const int cornerR = 15; + const int gap = 20; + + // Position: directly above the speed limit sign + int speedLimitY = height() - signH - margin; + QRect signRect(margin, speedLimitY - signH - gap, signW, signH); + + if (nightriderMode) { + // Nightrider: black background with colored border/text + QColor accentColor = isOver ? QColor(220, 50, 50) : QColor(50, 180, 80); + + p.setPen(QPen(accentColor, borderW)); + p.setBrush(QColor(0, 0, 0, 220)); + p.drawRoundedRect(signRect, cornerR, cornerR); + + QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin); + p.setPen(QPen(accentColor, innerBorderW)); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4); + + // "CRUISE" text + p.setPen(accentColor); + p.setFont(InterFont(26, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "CRUISE"); + + // "SET" text + p.setFont(InterFont(26, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 45, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SET"); + + // Cruise speed number + p.setFont(InterFont(90, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpCruiseWarningSpeed); + } else { + // Normal: colored background with white border/text + QColor bgColor = isOver ? QColor(200, 30, 30, 240) : QColor(40, 160, 60, 240); + QColor fgColor(255, 255, 255); + + p.setPen(QPen(fgColor, borderW)); + p.setBrush(bgColor); + p.drawRoundedRect(signRect, cornerR, cornerR); + + QRect innerRect = signRect.adjusted(innerMargin, innerMargin, -innerMargin, -innerMargin); + p.setPen(QPen(fgColor, innerBorderW)); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(innerRect, cornerR - 4, cornerR - 4); + + // "CRUISE" text + p.setPen(fgColor); + p.setFont(InterFont(26, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 15, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "CRUISE"); + + // "SET" text + p.setFont(InterFont(26, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 45, 0, 0), Qt::AlignTop | Qt::AlignHCenter, "SET"); + + // Cruise speed number + p.setFont(InterFont(90, QFont::Bold)); + p.drawText(innerRect.adjusted(0, 86, 0, 0), Qt::AlignCenter, clpCruiseWarningSpeed); + } + + p.restore(); +} + void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { QRect real_rect = p.fontMetrics().boundingRect(text); real_rect.moveCenter({x, y - real_rect.height() / 2}); @@ -543,6 +731,87 @@ void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &t p.drawText(real_rect.x(), real_rect.bottom(), text); } +// CLEARPILOT: System health overlay — shows metrics that indicate the system +// is overburdened or behind. Toggled via ClearpilotShowHealthMetrics param. +// Metrics (top→bottom): FPS, DROP, TEMP, CPU, MEM, FAN +// FPS: modeld framerate — 20 normally, 0 in park. Read from ModelFps memory +// param which modeld writes only on transition. +// DROP (%): modelV2 frameDropPerc — modeld losing frames; controlsd errors >20% +// TEMP (°C): deviceState.maxTempC — thermal throttling starts ~75, serious >88 +// CPU (%): max core from deviceState.cpuUsagePercent +// MEM (%): deviceState.memoryUsagePercent +// FAN (%): actual fan duty from peripheralState RPM (scaled to 6500 RPM = 100%) +// Each value color-codes green/yellow/red by severity. +void AnnotatedCameraWidget::drawHealthMetrics(QPainter &p) { + static bool enabled = Params().getBool("ClearpilotShowHealthMetrics"); + static int check_counter = 0; + // re-check the param every ~2s without a toggle signal path + if (++check_counter >= 40) { + check_counter = 0; + enabled = Params().getBool("ClearpilotShowHealthMetrics"); + } + if (!enabled) return; + + SubMaster &sm = *(uiState()->sm); + auto ds = sm["deviceState"].getDeviceState(); + auto mv = sm["modelV2"].getModelV2(); + auto ps = sm["peripheralState"].getPeripheralState(); + + int model_fps = paramsMemory.getInt("ModelFps"); + float drop_pct = mv.getFrameDropPerc(); + float temp_c = ds.getMaxTempC(); + int mem_pct = ds.getMemoryUsagePercent(); + int cpu_pct = 0; + for (auto v : ds.getCpuUsagePercent()) cpu_pct = std::max(cpu_pct, (int)v); + // Actual fan (not commanded): scale RPM to 0-100 using 6500 RPM as full scale + int fan_pct = std::min(100, (int)(ps.getFanSpeedRpm() * 100 / 6500)); + + auto color_for = [](float v, float warn, float crit) { + if (v >= crit) return QColor(0xff, 0x50, 0x50); // red + if (v >= warn) return QColor(0xff, 0xd0, 0x40); // yellow + return QColor(0xff, 0xff, 0xff); // white (ok) + }; + + struct Row { QString label; QString value; QColor color; }; + Row rows[] = { + {"FPS", QString::number(model_fps), QColor(0xff, 0xff, 0xff)}, + {"DROP", QString::number((int)drop_pct),color_for(drop_pct, 5.f, 15.f)}, + {"TEMP", QString::number((int)temp_c), color_for(temp_c, 75.f, 88.f)}, + {"CPU", QString::number(cpu_pct), color_for((float)cpu_pct, 75.f, 90.f)}, + {"MEM", QString::number(mem_pct), color_for((float)mem_pct, 70.f, 85.f)}, + {"FAN", QString::number(fan_pct), QColor(0xff, 0xff, 0xff)}, + }; + + p.save(); + p.setFont(InterFont(90, QFont::Bold)); + QFontMetrics fm = p.fontMetrics(); + int row_h = fm.height(); // natural line height at 90pt bold + int gap = 40; // requested 40px between values + int margin = 30; // requested 30px margin + int panel_w = 360; // fixed width — fits "TEMP 99" + int n = sizeof(rows) / sizeof(rows[0]); + int panel_h = n * row_h + (n - 1) * gap + 2 * margin; + int x = width() - panel_w - margin; + int y = height() - panel_h - margin; + + // black background + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 0, 0, 200)); + p.drawRoundedRect(QRect(x, y, panel_w, panel_h), 20, 20); + + // rows + int text_y = y + margin + fm.ascent(); + for (int i = 0; i < n; i++) { + p.setPen(rows[i].color); + // label left (shifted -50px per user request), value right + p.drawText(x + margin - 50, text_y, rows[i].label); + QRect vrect = fm.boundingRect(rows[i].value); + p.drawText(x + panel_w - margin - vrect.width(), text_y, rows[i].value); + text_y += row_h + gap; + } + p.restore(); +} + void AnnotatedCameraWidget::initializeGL() { CameraWidget::initializeGL(); qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION)); @@ -562,13 +831,6 @@ void AnnotatedCameraWidget::updateFrameMat() { s->fb_w = w; s->fb_h = h; - if (screenDisplayMode == 1 || screenDisplayMode == 2) { - // Render a black box instead of the video feed - QPainter painter(this); - painter.fillRect(0, 0, w, h, Qt::black); - return; - } - // Apply transformation such that video pixel coordinates match video // 1) Put (0, 0) in the middle of the video // 2) Apply same scaling as video @@ -580,31 +842,58 @@ void AnnotatedCameraWidget::updateFrameMat() { } void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { - if (screenDisplayMode == 2) { - return; - } - painter.save(); // CLEARPILOT: color channel code rewriten to allow custom colors SubMaster &sm = *(s->sm); + // CLEARPILOT: nightrider mode — outline only, no fill + bool outlineOnly = nightriderMode; + + // CLEARPILOT: in nightrider mode, hide all lines when not engaged + if (outlineOnly && edgeColor == bg_colors[STATUS_DISENGAGED]) { + painter.restore(); + return; + } + + // CLEARPILOT: nightrider lines are 1px wider (3 instead of 2) + int outlineWidth = outlineOnly ? 3 : 2; + // CLEARPILOT: lane lines 5% thinner than the generic outline (QPen accepts float width) + float laneLineWidth = outlineOnly ? (float)outlineWidth * 0.95f : (float)outlineWidth; + // lanelines for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) { - painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(scene.lane_line_probs[i], 0.0, 0.7))); + QColor lineColor = QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(scene.lane_line_probs[i], 0.0, 0.7)); + if (outlineOnly) { + painter.setPen(QPen(lineColor, laneLineWidth)); + painter.setBrush(Qt::NoBrush); + } else { + painter.setPen(Qt::NoPen); + painter.setBrush(lineColor); + } painter.drawPolygon(scene.lane_line_vertices[i]); } + if (outlineOnly) painter.setPen(Qt::NoPen); // road edges for (int i = 0; i < std::size(scene.road_edge_vertices); ++i) { - painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp(1.0 - scene.road_edge_stds[i], 0.0, 1.0))); + QColor edgeCol = QColor::fromRgbF(1.0, 0, 0, std::clamp(1.0 - scene.road_edge_stds[i], 0.0, 1.0)); + if (outlineOnly) { + painter.setPen(QPen(edgeCol, outlineWidth)); + painter.setBrush(Qt::NoBrush); + } else { + painter.setPen(Qt::NoPen); + painter.setBrush(edgeCol); + } painter.drawPolygon(scene.road_edge_vertices[i]); } + if (outlineOnly) painter.setPen(Qt::NoPen); // paint center lane path // QColor bg_colors[CHANGE_LANE_PATH_COLOR]; + // CLEARPILOT: read from paramsMemory; controlsd writes "no_lat_lane_change". bool is_no_lat_lane_change = paramsMemory.getBool("no_lat_lane_change"); QColor center_lane_color; @@ -665,12 +954,23 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { path_gradient.setColorAt(1.0, QColor(center_lane_color.red(), center_lane_color.green(), center_lane_color.blue(), static_cast(CENTER_LANE_ALPHA * 255 * 0.0))); } - painter.setBrush(path_gradient); + if (outlineOnly) { + // CLEARPILOT: in nightrider, the tire path outline is light blue at 3px. + // Uses a fixed light-blue instead of center_lane_color (which is status-tinted) so + // the path reads as a neutral guide, not as engagement/status feedback. + QColor lightBlue(153, 204, 255, 220); // #99CCFF light blue, mostly opaque + painter.setPen(QPen(lightBlue, 3)); + painter.setBrush(Qt::NoBrush); + } else { + painter.setPen(Qt::NoPen); + painter.setBrush(path_gradient); + } painter.drawPolygon(scene.track_vertices); + if (outlineOnly) painter.setPen(Qt::NoPen); } // Paint path edges ,Use current background color - if (edgeColor != bg_colors[STATUS_DISENGAGED]) { + if (edgeColor != bg_colors[STATUS_DISENGAGED] && !outlineOnly) { QLinearGradient edge_gradient; edge_gradient.setColorAt(0.0, QColor(edgeColor.red(), edgeColor.green(), edgeColor.blue(), static_cast(255))); edge_gradient.setColorAt(0.5, QColor(edgeColor.red(), edgeColor.green(), edgeColor.blue(), static_cast(255 * 0.8) )); @@ -686,18 +986,24 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { // Paint blindspot path if (scene.blind_spot_path) { - QLinearGradient bs(0, height(), 0, 0); - bs.setColorAt(0.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6)); - bs.setColorAt(0.5, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.4)); - bs.setColorAt(1.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.2)); - - painter.setBrush(bs); + QColor bsColor = QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6); + if (outlineOnly) { + painter.setPen(QPen(bsColor, outlineWidth)); + painter.setBrush(Qt::NoBrush); + } else { + QLinearGradient bs(0, height(), 0, 0); + bs.setColorAt(0.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6)); + bs.setColorAt(0.5, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.4)); + bs.setColorAt(1.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.2)); + painter.setBrush(bs); + } if (blindSpotLeft) { painter.drawPolygon(scene.track_adjacent_vertices[4]); } if (blindSpotRight) { painter.drawPolygon(scene.track_adjacent_vertices[5]); } + if (outlineOnly) painter.setPen(Qt::NoPen); } // Paint adjacent lane paths @@ -708,16 +1014,19 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { float maxLaneWidth = laneDetectionWidth * 1.5; auto paintLane = [=](QPainter &painter, const QPolygonF &lane, float laneWidth, bool blindspot) { - QLinearGradient al(0, height(), 0, 0); - bool redPath = laneWidth < minLaneWidth || laneWidth > maxLaneWidth || blindspot; float hue = redPath ? 0.0 : 120.0 * (laneWidth - minLaneWidth) / (maxLaneWidth - minLaneWidth); - al.setColorAt(0.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.6)); - al.setColorAt(0.5, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.4)); - al.setColorAt(1.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.2)); - - painter.setBrush(al); + if (outlineOnly) { + painter.setPen(QPen(QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.6), 2)); + painter.setBrush(Qt::NoBrush); + } else { + QLinearGradient al(0, height(), 0, 0); + al.setColorAt(0.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.6)); + al.setColorAt(0.5, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.4)); + al.setColorAt(1.0, QColor::fromHslF(hue / 360.0, 0.75, 0.50, 0.2)); + painter.setBrush(al); + } painter.drawPolygon(lane); painter.setFont(InterFont(30, QFont::DemiBold)); @@ -805,6 +1114,10 @@ void AnnotatedCameraWidget::paintEvent(QPaintEvent *event) { const cereal::ModelDataV2::Reader &model = sm["modelV2"].getModelV2(); const float v_ego = sm["carState"].getCarState().getVEgo(); + // CLEARPILOT: read display mode early — needed for camera suppression + displayMode = paramsMemory.getInt("ScreenDisplayMode"); + nightriderMode = (displayMode == 1 || displayMode == 4); + // draw camera frame { std::lock_guard lk(frame_lock); @@ -845,9 +1158,19 @@ void AnnotatedCameraWidget::paintEvent(QPaintEvent *event) { } else { CameraWidget::updateCalibration(DEFAULT_CALIBRATION); } + // CLEARPILOT: force CameraWidget bg to black in nightrider to prevent color bleed + if (nightriderMode) { + CameraWidget::setBackgroundColor(Qt::black); + } painter.beginNativePainting(); - CameraWidget::setFrameId(model.getFrameId()); - CameraWidget::paintGL(); + if (nightriderMode) { + // CLEARPILOT: black background, no camera feed + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + } else { + CameraWidget::setFrameId(model.getFrameId()); + CameraWidget::paintGL(); + } painter.endNativePainting(); } @@ -925,7 +1248,7 @@ void AnnotatedCameraWidget::initializeFrogPilotWidgets() { animationFrameIndex = (animationFrameIndex + 1) % totalFrames; }); - // Initialize the timer for the screen recorder + // CLEARPILOT: screen recorder disabled — replaced by dedicated dashcamd process // QTimer *record_timer = new QTimer(this); // connect(record_timer, &QTimer::timeout, this, [this]() { // if (recorder_btn) { @@ -1010,7 +1333,8 @@ void AnnotatedCameraWidget::updateFrogPilotWidgets() { } void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) { - if ((showAlwaysOnLateralStatusBar || showConditionalExperimentalStatusBar || roadNameUI)) { + // CLEARPILOT: only show status bar when telemetry is enabled + if (paramsMemory.getBool("TelemetryEnabled")) { drawStatusBar(p); } @@ -1022,8 +1346,7 @@ void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) { drawSLCConfirmation(p); } - // recorder_btn->setVisible(scene.screen_recorder && !mapOpen); - recorder_btn->setVisible(false); + // CLEARPILOT: screen recorder disabled, using dashcamd instead } void AnnotatedCameraWidget::drawLeadInfo(QPainter &p) { @@ -1212,7 +1535,30 @@ void AnnotatedCameraWidget::drawStatusBar(QPainter &p) { QString roadName = roadNameUI ? QString::fromStdString(paramsMemory.get("RoadName")) : QString(); - if (alwaysOnLateralActive && showAlwaysOnLateralStatusBar) { + // CLEARPILOT: telemetry status bar — show live stats when telemetry enabled + if (paramsMemory.getBool("TelemetryEnabled")) { + SubMaster &sm = *(uiState()->sm); + auto deviceState = sm["deviceState"].getDeviceState(); + int maxTempC = deviceState.getMaxTempC(); + int fanPct = deviceState.getFanSpeedPercentDesired(); + bool standstill = sm["carState"].getCarState().getStandstill(); + + static double last_model_status_t = 0; + static float model_status_fps = 0; + if (sm.updated("modelV2")) { + double now = millis_since_boot(); + if (last_model_status_t > 0) { + double dt = now - last_model_status_t; + if (dt > 0) model_status_fps = model_status_fps * 0.8 + (1000.0 / dt) * 0.2; + } + last_model_status_t = now; + } + + newStatus = QString("%1\u00B0C FAN %2% MDL %3") + .arg(maxTempC).arg(fanPct).arg(model_status_fps, 0, 'f', 0); + if (standstill) newStatus += " STANDSTILL"; + // CLEARPILOT: suppress "Always On Lateral active" status bar message + } else if (false && alwaysOnLateralActive && showAlwaysOnLateralStatusBar) { newStatus = tr("Always On Lateral active") + (tr(". Press the \"Cruise Control\" button to disable")); } else if (showConditionalExperimentalStatusBar) { newStatus = conditionalStatusMap[status != STATUS_DISENGAGED ? conditionalStatus : 0]; diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index ac053fc..9e7170e 100755 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -12,7 +12,7 @@ #include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/widgets/cameraview.h" -#include "selfdrive/frogpilot/screenrecorder/screenrecorder.h" +// #include "selfdrive/frogpilot/screenrecorder/screenrecorder.h" // DISABLED: using dashcamd instead const int btn_size = 192; const int img_size = (btn_size / 4) * 3; @@ -47,16 +47,31 @@ public: explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0); void updateState(const UIState &s); void updateLaneEdgeColor(QColor &bgColor); - int screenDisplayMode = 0; private: void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); void drawSpeedWidget(QPainter &p, int x, int y, const QString &title, const QString &speedLimitStr, QColor colorSpeed, int width = 176); + void drawSpeedLimitSign(QPainter &p); + void drawCruiseWarningSign(QPainter &p); + void drawHealthMetrics(QPainter &p); QVBoxLayout *main_layout; QPixmap dm_img; float speed; + bool has_gps_speed = false; + bool nightriderMode = false; + int displayMode = 0; QString speedUnit; + + // ClearPilot speed state (from params_memory, updated ~2Hz) + bool clpHasSpeed = false; + QString clpSpeedDisplay; + QString clpSpeedLimitDisplay; + QString clpSpeedUnit; + QString clpCruiseWarning; + QString clpCruiseWarningSpeed; + int clpParamFrame = 0; + float setSpeed; float speedLimit; bool is_cruise_set = false; @@ -89,8 +104,6 @@ private: Params paramsMemory{"/dev/shm/params"}; UIScene &scene; - ScreenRecorder *recorder_btn; - QHBoxLayout *bottom_layout; bool alwaysOnLateralActive; diff --git a/selfdrive/ui/qt/ready.cc b/selfdrive/ui/qt/ready.cc index aa82f52..313a5a4 100755 --- a/selfdrive/ui/qt/ready.cc +++ b/selfdrive/ui/qt/ready.cc @@ -30,11 +30,12 @@ ReadyWindow::ReadyWindow(QWidget *parent) : QWidget(parent) { timer = new QTimer(this); timer->callOnTimeout(this, &ReadyWindow::refresh); + uptime.start(); } void ReadyWindow::showEvent(QShowEvent *event) { refresh(); - timer->start(5 * 1000); + timer->start(2 * 1000); } void ReadyWindow::hideEvent(QHideEvent *event) { @@ -43,34 +44,94 @@ void ReadyWindow::hideEvent(QHideEvent *event) { void ReadyWindow::paintEvent(QPaintEvent *event) { QPainter painter(this); - QPixmap *img_shown = nullptr; + painter.fillRect(rect(), Qt::black); if (is_hot) { if (img_hot.isNull()) { img_hot.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/hot.png"); } - img_shown = &img_hot; + int x = (width() - img_hot.width()) / 2; + int y = (height() - img_hot.height()) / 2; + painter.drawPixmap(x, y, img_hot); } else { - if (img_ready.isNull()) { - img_ready.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready.png"); + // Boot logo — same rotation as spinner (bg.jpg is pre-rotated 90° CW for framebuffer) + if (img_bg.isNull()) { + QPixmap raw("/usr/comma/bg.jpg"); + if (!raw.isNull()) { + QTransform rot; + rot.rotate(-90); + img_bg = raw.transformed(rot); + } + } + if (!img_bg.isNull()) { + QPixmap scaled = img_bg.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + int x = (width() - scaled.width()) / 2; + int y = (height() - scaled.height()) / 2; + painter.drawPixmap(x, y, scaled); } - img_shown = &img_ready; - } - int x = (width() - img_shown->width()) / 2; - int y = (height() - img_shown->height()) / 2; - painter.drawPixmap(x, y, *img_shown); + if (error_msg.isEmpty() && !has_driven) { + // "READY!" 8-bit text sprite, 15% below center — only before first drive + static QPixmap ready_text("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png"); + if (!ready_text.isNull()) { + int tx = (width() - ready_text.width()) / 2; + int ty = height() / 2 + height() * 15 / 100; + painter.drawPixmap(tx, ty, ready_text); + } + } else { + // Error state: red text at 25% below center + QFont font("Inter", 50, QFont::Bold); + painter.setFont(font); + painter.setPen(QColor(0xFF, 0x44, 0x44)); + int ty = height() / 2 + height() * 25 / 100; + QRect text_rect(0, ty, width(), 100); + painter.drawText(text_rect, Qt::AlignHCenter | Qt::AlignTop, error_msg); + } + } } void ReadyWindow::refresh() { + bool changed = false; + + // Temperature check std::string bytes = params.get("Offroad_TemperatureTooHigh"); + bool was_hot = is_hot; if (!bytes.empty()) { auto doc = QJsonDocument::fromJson(bytes.data()); is_hot = true; cur_temp = doc["extra"].toString(); - update(); - } else if (is_hot) { + } else { is_hot = false; - update(); } + if (is_hot != was_hot) changed = true; + + // Error state checks (only when not hot — hot has its own display) + if (!is_hot) { + QString prev_error = error_msg; + + // Panda check — same logic as sidebar, with 10s grace period on startup + if (uptime.elapsed() > 10000 && + uiState()->scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { + error_msg = "PANDA NOT CONNECTED"; + } + // Car unrecognized check + else if (!params.get("Offroad_CarUnrecognized").empty()) { + error_msg = "CAR NOT RECOGNIZED"; + } + else { + error_msg = ""; + } + + if (error_msg != prev_error) changed = true; + } + + // Reset has_driven on ignition off→on (power cycle) + bool started = uiState()->scene.started; + if (!last_started && started) { + has_driven = false; + changed = true; + } + last_started = started; + + if (changed) update(); } \ No newline at end of file diff --git a/selfdrive/ui/qt/ready.h b/selfdrive/ui/qt/ready.h index c2bb4d8..a428c99 100755 --- a/selfdrive/ui/qt/ready.h +++ b/selfdrive/ui/qt/ready.h @@ -8,8 +8,9 @@ #include #include #include +#include #include - + #include "common/util.h" #include "selfdrive/ui/ui.h" @@ -17,6 +18,7 @@ class ReadyWindow : public QWidget { Q_OBJECT public: ReadyWindow(QWidget* parent = nullptr); + bool has_driven = false; private: void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; @@ -26,6 +28,9 @@ private: QTimer* timer; bool is_hot = false; QString cur_temp; - QPixmap img_ready; + QString error_msg; // non-empty = show red error instead of READY! + QElapsedTimer uptime; + bool last_started = false; + QPixmap img_bg; QPixmap img_hot; }; \ No newline at end of file diff --git a/selfdrive/ui/qt/spinner b/selfdrive/ui/qt/spinner index 6152ad6..aa5dbb7 100755 Binary files a/selfdrive/ui/qt/spinner and b/selfdrive/ui/qt/spinner differ diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index 6df6f92..0a53514 100755 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -1,6 +1,9 @@ #include "selfdrive/ui/qt/window.h" #include +#include + +#include #include "system/hardware/hw.h" @@ -11,6 +14,7 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { homeWindow = new HomeWindow(this); main_layout->addWidget(homeWindow); QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); + QObject::connect(homeWindow, &HomeWindow::openStatus, this, &MainWindow::openStatus); QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); settingsWindow = new SettingsWindow(this); @@ -24,6 +28,11 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { homeWindow->showDriverView(true); }); + // CLEARPILOT: Status window + statusWindow = new StatusWindow(this); + main_layout->addWidget(statusWindow); + QObject::connect(statusWindow, &StatusWindow::closeStatus, this, &MainWindow::closeSettings); + onboardingWindow = new OnboardingWindow(this); main_layout->addWidget(onboardingWindow); QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() { @@ -35,13 +44,17 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { if (!offroad) { - closeSettings(); + // CLEARPILOT: just switch to homeWindow, don't show sidebar + // HomeWindow::offroadTransition handles the internal view + main_layout->setCurrentWidget(homeWindow); } }); QObject::connect(device(), &Device::interactiveTimeout, [=]() { - if (main_layout->currentWidget() == settingsWindow) { - closeSettings(); + // CLEARPILOT: on timeout, return to splash/onroad (not ClearPilotPanel) + if (main_layout->currentWidget() != homeWindow) { + main_layout->setCurrentWidget(homeWindow); } + homeWindow->offroadTransition(!uiState()->scene.started); }); // load fonts @@ -63,6 +76,74 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { } )"); setAttribute(Qt::WA_NoSystemBackground); + + // CLEARPILOT: UI introspection RPC server + zmq_ctx = zmq_ctx_new(); + zmq_sock = zmq_socket(zmq_ctx, ZMQ_REP); + zmq_bind(zmq_sock, "ipc:///tmp/clearpilot_ui_rpc"); + int fd; + size_t fd_sz = sizeof(fd); + zmq_getsockopt(zmq_sock, ZMQ_FD, &fd, &fd_sz); + rpc_notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + connect(rpc_notifier, &QSocketNotifier::activated, this, &MainWindow::handleRpcRequest); +} + +void MainWindow::handleRpcRequest() { + int events = 0; + size_t events_sz = sizeof(events); + zmq_getsockopt(zmq_sock, ZMQ_EVENTS, &events, &events_sz); + if (!(events & ZMQ_POLLIN)) return; + + char buf[256]; + int rc = zmq_recv(zmq_sock, buf, sizeof(buf) - 1, ZMQ_DONTWAIT); + if (rc < 0) return; + buf[rc] = 0; + + QString response; + if (strcmp(buf, "dump") == 0) { + response = dumpWidgetTree(this); + } else { + response = "unknown command"; + } + + QByteArray resp = response.toUtf8(); + zmq_send(zmq_sock, resp.data(), resp.size(), 0); +} + +QString MainWindow::dumpWidgetTree(QWidget *w, int depth) { + QString result; + QString indent(depth * 2, ' '); + QString className = w->metaObject()->className(); + QString name = w->objectName().isEmpty() ? "(no name)" : w->objectName(); + bool visible = w->isVisible(); + QRect geo = w->geometry(); + + result += QString("%1%2 [%3] vis=%4 geo=%5,%6 %7x%8") + .arg(indent, className, name) + .arg(visible ? "Y" : "N") + .arg(geo.x()).arg(geo.y()).arg(geo.width()).arg(geo.height()); + + // Show stacked layout/widget current index + if (auto *sl = w->findChild(QString(), Qt::FindDirectChildrenOnly)) { + QWidget *cur = sl->currentWidget(); + QString curClass = cur ? cur->metaObject()->className() : "null"; + result += QString(" stack_cur=%1/%2(%3)").arg(sl->currentIndex()).arg(sl->count()).arg(curClass); + } + if (auto *sw = qobject_cast(w)) { + QWidget *cur = sw->currentWidget(); + QString curClass = cur ? cur->metaObject()->className() : "null"; + result += QString(" stack_cur=%1/%2(%3)").arg(sw->currentIndex()).arg(sw->count()).arg(curClass); + } + + result += "\n"; + + for (QObject *child : w->children()) { + QWidget *cw = qobject_cast(child); + if (cw && depth < 4) { + result += dumpWidgetTree(cw, depth + 1); + } + } + return result; } void MainWindow::openSettings(int index, const QString ¶m) { @@ -70,6 +151,10 @@ void MainWindow::openSettings(int index, const QString ¶m) { settingsWindow->setCurrentPanel(index, param); } +void MainWindow::openStatus() { + main_layout->setCurrentWidget(statusWindow); +} + void MainWindow::closeSettings() { main_layout->setCurrentWidget(homeWindow); @@ -86,6 +171,16 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) { case QEvent::TouchEnd: case QEvent::MouseButtonPress: case QEvent::MouseMove: { + // CLEARPILOT: tap while screen-off (mode 3) -> wake to auto-normal (mode 0) + Params pmem{"/dev/shm/params"}; + if (!device()->isAwake()) { + if (pmem.getInt("ScreenDisplayMode") == 3) { + pmem.putInt("ScreenDisplayMode", 0); + } + } + // CLEARPILOT: reset shutdown timer on any screen touch + static int touch_counter = 0; + pmem.put("ShutdownTouchReset", std::to_string(++touch_counter)); // ignore events when device is awakened by resetInteractiveTimeout ignore = !device()->isAwake(); device()->resetInteractiveTimeout(uiState()->scene.screen_timeout, uiState()->scene.screen_timeout_onroad); @@ -96,3 +191,215 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) { } return ignore; } + +// CLEARPILOT: Status window — live system stats, collected on background thread + +#include +#include +#include +#include + +static QString readFile(const QString &path) { + QFile f(path); + if (f.open(QIODevice::ReadOnly)) return QString(f.readAll()).trimmed(); + return ""; +} + +static QString shellCmd(const QString &cmd) { + QProcess p; + p.start("bash", QStringList() << "-c" << cmd); + p.waitForFinished(1000); + return QString(p.readAllStandardOutput()).trimmed(); +} + +static StatusWindow::StatusData collectStatus() { + StatusWindow::StatusData d; + + d.time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); + d.storage = shellCmd("df -h /data | tail -1 | awk '{print $3 \" / \" $2 \" (\" $5 \" used)\"}'"); + + // RAM from /proc/meminfo (no subprocess needed) + QString meminfo = readFile("/proc/meminfo"); + long total = 0, avail = 0; + for (const QString &line : meminfo.split('\n')) { + if (line.startsWith("MemTotal:")) total = line.split(QRegExp("\\s+"))[1].toLong(); + if (line.startsWith("MemAvailable:")) avail = line.split(QRegExp("\\s+"))[1].toLong(); + } + if (total > 0) { + long used = total - avail; + d.ram = QString("%1 / %2 MB").arg(used / 1024).arg(total / 1024); + } + + // Load from /proc/loadavg + QString loadavg = readFile("/proc/loadavg"); + QStringList parts = loadavg.split(' '); + if (parts.size() >= 3) { + d.load = QString("%1 %2 %3").arg(parts[0], parts[1], parts[2]); + } + + // Temperature + QString temps = shellCmd("cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -rn | head -1"); + if (!temps.isEmpty()) { + d.temp_c = temps.toLong() / 1000.0f; + d.temp = QString("%1\u00B0C").arg(d.temp_c, 0, 'f', 1); + } + + // Fan + QString fan = shellCmd("cat /sys/class/hwmon/hwmon*/fan1_input 2>/dev/null | head -1"); + if (fan.isEmpty()) fan = readFile("/dev/shm/params/d/LastFanSpeed"); + d.fan = fan.isEmpty() ? QString::fromUtf8("\u2014") : fan + " RPM"; + + // Network + d.ip = shellCmd("ip route get 1.1.1.1 2>/dev/null | head -1 | awk '{print $7}'"); + d.wifi = shellCmd("iwconfig wlan0 2>/dev/null | grep -oP 'ESSID:\"\\K[^\"]*'"); + + // VPN + QString tun = shellCmd("ip link show tun0 2>/dev/null | head -1"); + if (tun.contains("UP")) { + d.vpn_status = "up"; + d.vpn_ip = shellCmd("ip addr show tun0 2>/dev/null | grep 'inet ' | awk '{print $2}'"); + } + + // GPS + d.gps = readFile("/data/params/d/LastGPSPosition"); + + // Telemetry + d.telemetry = readFile("/data/params/d/TelemetryEnabled"); + + // Dashcam + d.dashcam_state = readFile("/dev/shm/params/d/DashcamState"); + if (d.dashcam_state.isEmpty()) d.dashcam_state = "stopped"; + d.dashcam_frames = readFile("/dev/shm/params/d/DashcamFrames"); + + // Panda: checked on UI thread in applyResults() via scene.pandaType + + return d; +} + +StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) { + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(50, 60, 50, 40); + layout->setSpacing(0); + + // Status rows + auto makeRow = [&](const QString &label) -> QLabel* { + QHBoxLayout *row = new QHBoxLayout(); + row->setContentsMargins(20, 0, 20, 0); + + QLabel *name = new QLabel(label); + name->setStyleSheet("color: grey; font-size: 38px;"); + name->setFixedWidth(350); + row->addWidget(name); + + QLabel *value = new QLabel("—"); + value->setStyleSheet("color: white; font-size: 38px;"); + row->addWidget(value); + row->addStretch(); + + layout->addLayout(row); + layout->addSpacing(12); + return value; + }; + + time_label = makeRow("Time"); + storage_label = makeRow("Storage"); + ram_label = makeRow("Memory"); + load_label = makeRow("Load"); + temp_label = makeRow("Temperature"); + fan_label = makeRow("Fan Speed"); + panda_label = makeRow("Panda"); + ip_label = makeRow("IP Address"); + wifi_label = makeRow("WiFi"); + vpn_label = makeRow("VPN"); + gps_label = makeRow("GPS"); + telemetry_label = makeRow("Telemetry"); + dashcam_label = makeRow("Dashcam"); + + layout->addStretch(); + + setStyleSheet("StatusWindow { background-color: black; }"); + + connect(&watcher, &QFutureWatcher::finished, this, &StatusWindow::applyResults); + + QTimer *timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &StatusWindow::kickRefresh); + timer->start(1000); +} + +void StatusWindow::kickRefresh() { + if (!isVisible() || collecting) return; + collecting = true; + watcher.setFuture(QtConcurrent::run(collectStatus)); +} + +void StatusWindow::applyResults() { + collecting = false; + StatusData d = watcher.result(); + + time_label->setText(d.time); + storage_label->setText(d.storage); + if (!d.ram.isEmpty()) ram_label->setText(d.ram); + if (!d.load.isEmpty()) load_label->setText(d.load); + + if (!d.temp.isEmpty()) { + temp_label->setText(d.temp); + temp_label->setStyleSheet(d.temp_c > 70 ? "color: #ff4444; font-size: 38px;" : + d.temp_c > 55 ? "color: #ffaa00; font-size: 38px;" : + "color: white; font-size: 38px;"); + } + + fan_label->setText(d.fan); + + // Panda: same check as sidebar — read scene.pandaType on UI thread + if (uiState()->scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) { + panda_label->setText("Connected"); + panda_label->setStyleSheet("color: #17c44d; font-size: 38px;"); + } else { + panda_label->setText("Not connected"); + panda_label->setStyleSheet("color: #ff4444; font-size: 38px;"); + } + + ip_label->setText(d.ip.isEmpty() ? "No connection" : d.ip); + wifi_label->setText(d.wifi.isEmpty() ? "Not connected" : d.wifi); + + if (d.vpn_status == "up") { + vpn_label->setText("Connected (" + d.vpn_ip + ")"); + vpn_label->setStyleSheet("color: #17c44d; font-size: 38px;"); + } else { + vpn_label->setText("Not connected"); + vpn_label->setStyleSheet("color: #ff4444; font-size: 38px;"); + } + + if (d.gps.isEmpty()) { + gps_label->setText("No fix"); + gps_label->setStyleSheet("color: #ff4444; font-size: 38px;"); + } else { + gps_label->setText(d.gps); + gps_label->setStyleSheet("color: white; font-size: 38px;"); + } + + if (d.telemetry == "1") { + telemetry_label->setText("Enabled"); + telemetry_label->setStyleSheet("color: #17c44d; font-size: 38px;"); + } else { + telemetry_label->setText("Disabled"); + telemetry_label->setStyleSheet("color: grey; font-size: 38px;"); + } + + if (d.dashcam_state == "recording") { + QString text = "Recording"; + if (!d.dashcam_frames.isEmpty() && d.dashcam_frames != "0") text += " (" + d.dashcam_frames + " frames)"; + dashcam_label->setText(text); + dashcam_label->setStyleSheet("color: #17c44d; font-size: 38px;"); + } else if (d.dashcam_state == "waiting") { + dashcam_label->setText("Waiting"); + dashcam_label->setStyleSheet("color: #ffaa00; font-size: 38px;"); + } else { + dashcam_label->setText("Stopped"); + dashcam_label->setStyleSheet("color: #ff4444; font-size: 38px;"); + } +} + +void StatusWindow::mousePressEvent(QMouseEvent *e) { + emit closeStatus(); +} diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h index f1389c2..2897cb1 100755 --- a/selfdrive/ui/qt/window.h +++ b/selfdrive/ui/qt/window.h @@ -1,12 +1,59 @@ #pragma once +#include #include +#include +#include +#include #include #include "selfdrive/ui/qt/home.h" #include "selfdrive/ui/qt/offroad/onboarding.h" #include "selfdrive/ui/qt/offroad/settings.h" +class StatusWindow : public QFrame { + Q_OBJECT + +public: + explicit StatusWindow(QWidget *parent = 0); + + struct StatusData { + QString time, storage, ram, load, temp, fan, ip, wifi; + QString vpn_status, vpn_ip, gps, telemetry; + QString dashcam_state, dashcam_frames; + float temp_c = 0; + }; + +protected: + void mousePressEvent(QMouseEvent *e) override; + +signals: + void closeStatus(); + +private slots: + void kickRefresh(); + void applyResults(); + +private: + + QFutureWatcher watcher; + bool collecting = false; + + QLabel *storage_label; + QLabel *ram_label; + QLabel *load_label; + QLabel *temp_label; + QLabel *fan_label; + QLabel *ip_label; + QLabel *wifi_label; + QLabel *vpn_label; + QLabel *gps_label; + QLabel *time_label; + QLabel *telemetry_label; + QLabel *dashcam_label; + QLabel *panda_label; +}; + class MainWindow : public QWidget { Q_OBJECT @@ -16,13 +63,24 @@ public: private: bool eventFilter(QObject *obj, QEvent *event) override; void openSettings(int index = 0, const QString ¶m = ""); + void openStatus(); void closeSettings(); + QString dumpWidgetTree(QWidget *w, int depth = 0); QStackedLayout *main_layout; HomeWindow *homeWindow; SettingsWindow *settingsWindow; + StatusWindow *statusWindow; OnboardingWindow *onboardingWindow; + // CLEARPILOT: UI introspection RPC + void *zmq_ctx = nullptr; + void *zmq_sock = nullptr; + QSocketNotifier *rpc_notifier = nullptr; + // FrogPilot variables Params params; + +private slots: + void handleRpcRequest(); }; diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index b827526..c9a8856 100755 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -818,15 +818,15 @@ OffroadHome UPDATE - تحديث + تحديث ALERTS - التنبهات + التنبهات ALERT - تنبيه + تنبيه diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 415db15..875bc8d 100755 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -771,15 +771,15 @@ OffroadHome UPDATE - Aktualisieren + Aktualisieren ALERTS - HINWEISE + HINWEISE ALERT - HINWEIS + HINWEIS diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index be97ab0..c76e64c 100755 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -814,15 +814,15 @@ OffroadHome UPDATE - MISE À JOUR + MISE À JOUR ALERTS - ALERTES + ALERTES ALERT - ALERTE + ALERTE diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 40e25b5..27d807e 100755 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -770,15 +770,15 @@ OffroadHome UPDATE - 更新 + 更新 ALERTS - 警告 + 警告 ALERT - 警告 + 警告 diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index f943e65..072eee9 100755 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -813,15 +813,15 @@ OffroadHome UPDATE - 업데이트 + 업데이트 ALERTS - 알림 + 알림 ALERT - 알림 + 알림 diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index eca790e..ca1a193 100755 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -814,15 +814,15 @@ OffroadHome UPDATE - ATUALIZAÇÃO + ATUALIZAÇÃO ALERTS - ALERTAS + ALERTAS ALERT - ALERTA + ALERTA diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index cf4039c..89fbac2 100755 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -813,15 +813,15 @@ OffroadHome UPDATE - อัปเดต + อัปเดต ALERTS - การแจ้งเตือน + การแจ้งเตือน ALERT - การแจ้งเตือน + การแจ้งเตือน diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 70dd269..f8c8eef 100755 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -770,15 +770,15 @@ OffroadHome UPDATE - GÜNCELLE + GÜNCELLE ALERTS - UYARILAR + UYARILAR ALERT - UYARI + UYARI diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 26cf5eb..768a426 100755 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -813,15 +813,15 @@ OffroadHome UPDATE - 更新 + 更新 ALERTS - 警报 + 警报 ALERT - 警报 + 警报 diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 6ac3624..33c6f59 100755 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -813,15 +813,15 @@ OffroadHome UPDATE - 更新 + 更新 ALERTS - 提醒 + 提醒 ALERT - 提醒 + 提醒 diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 93c95e0..72f68c4 100755 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -93,6 +93,9 @@ void update_model(UIState *s, if (plan_position.getX().size() < model.getPosition().getX().size()) { plan_position = model.getPosition(); } + // CLEARPILOT: guard against empty model data (bench mode, no modeld running) + if (plan_position.getX().size() == 0) return; + float max_distance = scene.unlimited_road_ui_length ? *(plan_position.getX().end() - 1) : std::clamp(*(plan_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE); @@ -115,8 +118,11 @@ void update_model(UIState *s, } // update path + // CLEARPILOT: read from paramsMemory; controlsd writes "no_lat_lane_change" + // there. (Broken used a custom cereal field; we keep the param-based wiring.) + bool no_lat_lane_change = paramsMemory.getBool("no_lat_lane_change"); float path; - if (paramsMemory.getBool("no_lat_lane_change")) { + if (no_lat_lane_change) { path = (float)LANE_CHANGE_NO_LAT_PATH_WIDTH / 20; // Release: better calc for EU users } else { path = (float)CENTER_LANE_WIDTH / 20; // Release: better calc for EU users @@ -391,6 +397,7 @@ void ui_update_frogpilot_params(UIState *s) { scene.screen_brightness = screen_management ? params.getInt("ScreenBrightness") : 101; scene.screen_brightness_onroad = screen_management ? params.getInt("ScreenBrightnessOnroad") : 101; scene.screen_recorder = screen_management && params.getBool("ScreenRecorder"); + scene.screen_recorder_debug = params.getBool("ScreenRecorderDebug"); scene.screen_timeout = screen_management ? params.getInt("ScreenTimeout") : 120; scene.screen_timeout_onroad = screen_management ? params.getInt("ScreenTimeoutOnroad") : 10; scene.standby_mode = screen_management && params.getBool("StandbyMode"); @@ -435,9 +442,10 @@ void UIState::updateStatus() { UIState::UIState(QObject *parent) : QObject(parent) { sm = std::make_unique>({ "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", - "pandaStates", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2", + "pandaStates", "peripheralState", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2", "wideRoadCameraState", "managerState", "uiPlan", "liveTorqueParameters", "frogpilotCarControl", "frogpilotDeviceState", "frogpilotPlan", + "gpsLocation", }); Params params; @@ -551,7 +559,10 @@ void Device::updateWakefulness(const UIState &s) { } if (ignition_state_changed) { - if (ignition_on && s.scene.screen_brightness_onroad == 0 && !s.scene.standby_mode) { + if (!ignition_on) { + // CLEARPILOT: ignition on→off blanks the screen immediately (tap still wakes). + resetInteractiveTimeout(0, 0); + } else if (s.scene.screen_brightness_onroad == 0 && !s.scene.standby_mode) { resetInteractiveTimeout(0, 0); } else { resetInteractiveTimeout(s.scene.screen_timeout, s.scene.screen_timeout_onroad); @@ -560,7 +571,10 @@ void Device::updateWakefulness(const UIState &s) { emit interactiveTimeout(); } - if (s.scene.screen_brightness_onroad != 0) { + // CLEARPILOT: ScreenDisplayMode 3 = screen off — override awake state + if (paramsMemory.getInt("ScreenDisplayMode") == 3) { + setAwake(false); + } else if (s.scene.screen_brightness_onroad != 0) { setAwake(s.scene.ignition || interactive_timeout > 0); } else { setAwake(interactive_timeout > 0); diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 78dc059..85368c3 100755 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -231,6 +231,7 @@ typedef struct UIScene { bool road_name_ui; bool rotating_wheel; bool screen_recorder; + bool screen_recorder_debug; bool show_aol_status_bar; bool show_cem_status_bar; bool show_jerk;