Removed nightrider exception that kept onroad UI visible in park. Shifting to park from nightrider mode now auto-switches to screen off. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
452 lines
15 KiB
C++
Executable File
452 lines
15 KiB
C++
Executable File
#include "selfdrive/ui/qt/home.h"
|
|
|
|
#include <cstdlib>
|
|
#include <QHBoxLayout>
|
|
#include <QMouseEvent>
|
|
#include <QStackedWidget>
|
|
#include <QVBoxLayout>
|
|
|
|
#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<cereal::Event>().getLiveCalibration();
|
|
if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) {
|
|
double pitch = calib.getRpyCalib()[1] * (180 / M_PI);
|
|
double yaw = calib.getRpyCalib()[2] * (180 / M_PI);
|
|
desc += 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<ButtonControl *>(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<QPushButton *>()) {
|
|
if (btn->text().contains("APN", Qt::CaseInsensitive)) {
|
|
// Hide the parent AbstractControl frame, not just the button
|
|
if (auto *frame = qobject_cast<QFrame *>(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<QPair<QString, QWidget *>> 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);
|
|
}
|