diff --git a/selfdrive/assets/img_circled_check.svg b/selfdrive/assets/img_circled_check.svg new file mode 100644 index 0000000000..1852ba947a --- /dev/null +++ b/selfdrive/assets/img_circled_check.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5687faf4cb22bece0405893671651eb13e8eda393987610f493ee5e05eac61d +size 439 diff --git a/selfdrive/assets/img_continue_triangle.svg b/selfdrive/assets/img_continue_triangle.svg new file mode 100644 index 0000000000..ee9db369a4 --- /dev/null +++ b/selfdrive/assets/img_continue_triangle.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1764c0c93703481b2ced63bc35c5a23a6c12388ca690ce4cf577b3b478a08b69 +size 197 diff --git a/selfdrive/ui/qt/api.cc b/selfdrive/ui/qt/api.cc index e604a5c09b..330a436a68 100644 --- a/selfdrive/ui/qt/api.cc +++ b/selfdrive/ui/qt/api.cc @@ -67,18 +67,23 @@ QString create_jwt(const QJsonObject &payloads, int expiry) { } // namespace CommaApi -HttpRequest::HttpRequest(QObject *parent, const QString &requestURL, bool create_jwt_) : create_jwt(create_jwt_), QObject(parent) { +HttpRequest::HttpRequest(QObject *parent, const QString &requestURL, bool create_jwt_, + int timeout) : create_jwt(create_jwt_), QObject(parent) { networkAccessManager = new QNetworkAccessManager(this); reply = NULL; networkTimer = new QTimer(this); networkTimer->setSingleShot(true); - networkTimer->setInterval(20000); + networkTimer->setInterval(timeout); connect(networkTimer, &QTimer::timeout, this, &HttpRequest::requestTimeout); sendRequest(requestURL); } +bool HttpRequest::active() { + return reply != NULL; +} + void HttpRequest::sendRequest(const QString &requestURL) { QString token; if(create_jwt) { @@ -105,11 +110,13 @@ void HttpRequest::requestTimeout() { // This function should always emit something void HttpRequest::requestFinished() { + bool success = false; if (reply->error() != QNetworkReply::OperationCanceledError) { networkTimer->stop(); QString response = reply->readAll(); if (reply->error() == QNetworkReply::NoError) { + success = true; emit receivedResponse(response); } else { qDebug() << reply->errorString(); @@ -118,6 +125,7 @@ void HttpRequest::requestFinished() { } else { emit timeoutResponse("timeout"); } + emit requestDone(success); reply->deleteLater(); reply = NULL; } diff --git a/selfdrive/ui/qt/api.h b/selfdrive/ui/qt/api.h index 8aa01ae2b5..584697a893 100644 --- a/selfdrive/ui/qt/api.h +++ b/selfdrive/ui/qt/api.h @@ -20,8 +20,9 @@ class HttpRequest : public QObject { Q_OBJECT public: - explicit HttpRequest(QObject* parent, const QString &requestURL, bool create_jwt_ = true); + explicit HttpRequest(QObject* parent, const QString &requestURL, bool create_jwt_ = true, int timeout = 20000); void sendRequest(const QString &requestURL); + bool active(); protected: QNetworkReply *reply; @@ -36,6 +37,7 @@ private slots: void requestFinished(); signals: + void requestDone(bool success); void receivedResponse(const QString &response); void failedResponse(const QString &errorString); void timeoutResponse(const QString &errorString); diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc index e6bbf64f0e..e06e677a6f 100644 --- a/selfdrive/ui/qt/setup/setup.cc +++ b/selfdrive/ui/qt/setup/setup.cc @@ -10,20 +10,21 @@ #include #include "selfdrive/hardware/hw.h" - +#include "selfdrive/ui/qt/api.h" +#include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/offroad/networking.h" #include "selfdrive/ui/qt/widgets/input.h" -#include "selfdrive/ui/qt/qt_window.h" -#define USER_AGENT "AGNOSSetup-0.1" +const char* USER_AGENT = "AGNOSSetup-0.1"; +const QString DASHCAM_URL = "https://dashcam.comma.ai"; -void Setup::download(QString url) { - QCoreApplication::processEvents(QEventLoop::AllEvents, 1000); - setCurrentIndex(count() - 2); +const QString TEST_URL = "https://api.commadotai.com/v1/me"; +void Setup::download(QString url) { CURL *curl = curl_easy_init(); if (!curl) { - emit downloadFailed(); + emit finished(false); + return; } char tmpfile[] = "/tmp/installer_XXXXXX"; @@ -38,121 +39,245 @@ void Setup::download(QString url) { int ret = curl_easy_perform(curl); if (ret != CURLE_OK) { - emit downloadFailed(); + emit finished(false); + return; } curl_easy_cleanup(curl); fclose(fp); rename(tmpfile, "/tmp/installer"); + emit finished(true); } -QLabel * title_label(QString text) { - QLabel *l = new QLabel(text); - l->setStyleSheet(R"( - font-size: 100px; - font-weight: 500; - )"); - return l; -} - -QWidget * Setup::build_page(QString title, QWidget *content, bool next, bool prev) { +QWidget * Setup::getting_started() { QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setMargin(50); - main_layout->addWidget(title_label(title), 0, Qt::AlignLeft | Qt::AlignTop); + QHBoxLayout *main_layout = new QHBoxLayout(widget); + main_layout->setMargin(0); - main_layout->addWidget(content); + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setContentsMargins(165, 280, 100, 0); + main_layout->addLayout(vlayout); - QHBoxLayout *nav_layout = new QHBoxLayout(); + QLabel *title = new QLabel("Getting Started"); + title->setStyleSheet("font-size: 90px; font-weight: 500;"); + vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - if (prev) { - QPushButton *back_btn = new QPushButton("Back"); - nav_layout->addWidget(back_btn, 1, Qt::AlignBottom | Qt::AlignLeft); - QObject::connect(back_btn, &QPushButton::released, this, &Setup::prevPage); - } + vlayout->addSpacing(90); + QLabel *desc = new QLabel("Before we get on the road, let’s finish installation and cover some details."); + desc->setWordWrap(true); + desc->setStyleSheet("font-size: 80px; font-weight: 300;"); + vlayout->addWidget(desc, 0, Qt::AlignTop | Qt::AlignLeft); - if (next) { - QPushButton *continue_btn = new QPushButton("Continue"); - nav_layout->addWidget(continue_btn, 0, Qt::AlignBottom | Qt::AlignRight); - QObject::connect(continue_btn, &QPushButton::released, this, &Setup::nextPage); - } + vlayout->addStretch(); - main_layout->addLayout(nav_layout, 0); - return widget; -} + QPushButton *btn = new QPushButton(); + btn->setIcon(QIcon("../../../assets/img_continue_triangle.svg")); + btn->setIconSize(QSize(54, 106)); + btn->setFixedSize(310, 1080); + btn->setProperty("primary", true); + btn->setStyleSheet("border: none;"); + main_layout->addWidget(btn, 0, Qt::AlignRight); + QObject::connect(btn, &QPushButton::clicked, this, &Setup::nextPage); -QWidget * Setup::getting_started() { - QLabel *body = new QLabel("Before we get on the road, let's finish\ninstallation and cover some details."); - body->setAlignment(Qt::AlignHCenter); - body->setStyleSheet(R"(font-size: 80px;)"); - return build_page("Getting Started", body, true, false); + return widget; } QWidget * Setup::network_setup() { + QWidget *widget = new QWidget(); + QVBoxLayout *main_layout = new QVBoxLayout(widget); + main_layout->setContentsMargins(55, 50, 55, 50); + + // title + QLabel *title = new QLabel("Connect to WiFi"); + title->setStyleSheet("font-size: 90px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); + + // wifi widget Networking *wifi = new Networking(this, false); - return build_page("Connect to WiFi", wifi, true, true); + main_layout->addWidget(wifi, 1); + + // back + continue buttons + QHBoxLayout *blayout = new QHBoxLayout; + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + QPushButton *back = new QPushButton("Back"); + back->setObjectName("navBtn"); + QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); + blayout->addWidget(back); + + QPushButton *cont = new QPushButton(); + cont->setObjectName("navBtn"); + QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); + blayout->addWidget(cont); + + // setup timer for testing internet connection + HttpRequest *request = new HttpRequest(this, TEST_URL, false, 2500); + QObject::connect(request, &HttpRequest::requestDone, [=](bool success) { + cont->setEnabled(success); + cont->setText(success ? "Continue" : "Waiting for internet"); + repaint(); + }); + QTimer *timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, [=]() { + if (!request->active() && cont->isVisible()) { + request->sendRequest(TEST_URL); + } + }); + timer->start(1000); + + return widget; +} + +QWidget * radio_button(QString title, QButtonGroup *group) { + QPushButton *btn = new QPushButton(title); + btn->setCheckable(true); + group->addButton(btn); + btn->setStyleSheet(R"( + QPushButton { + height: 230; + padding-left: 100px; + padding-right: 100px; + text-align: left; + font-size: 80px; + font-weight: 400; + border-radius: 10px; + background-color: #4F4F4F; + } + QPushButton:checked { + background-color: #465BEA; + } + )"); + + // checkmark icon + QPixmap pix("../../../assets/img_circled_check.svg"); + btn->setIcon(pix); + btn->setIconSize(QSize(0, 0)); + btn->setLayoutDirection(Qt::RightToLeft); + QObject::connect(btn, &QPushButton::toggled, [=](bool checked) { + btn->setIconSize(checked ? QSize(104, 104) : QSize(0, 0)); + }); + return btn; } QWidget * Setup::software_selection() { QWidget *widget = new QWidget(); QVBoxLayout *main_layout = new QVBoxLayout(widget); + main_layout->setContentsMargins(55, 50, 55, 50); + main_layout->setSpacing(0); - QPushButton *dashcam_btn = new QPushButton("Dashcam"); - main_layout->addWidget(dashcam_btn); - QObject::connect(dashcam_btn, &QPushButton::released, this, [=]() { - this->download("https://dashcam.comma.ai"); - }); + // title + QLabel *title = new QLabel("Choose Software to Install"); + title->setStyleSheet("font-size: 90px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); main_layout->addSpacing(50); - QPushButton *custom_btn = new QPushButton("Custom"); - main_layout->addWidget(custom_btn); - QObject::connect(custom_btn, &QPushButton::released, this, [=]() { - QString input_url = InputDialog::getText("Enter URL", this); - if (input_url.size()) { - this->download(input_url); + // dashcam + custom radio buttons + QButtonGroup *group = new QButtonGroup(widget); + group->setExclusive(true); + + QWidget *dashcam = radio_button("Dashcam", group); + main_layout->addWidget(dashcam); + + main_layout->addSpacing(30); + + QWidget *custom = radio_button("Custom Software", group); + main_layout->addWidget(custom); + + main_layout->addStretch(); + + // back + continue buttons + QHBoxLayout *blayout = new QHBoxLayout; + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + QPushButton *back = new QPushButton("Back"); + back->setObjectName("navBtn"); + QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); + blayout->addWidget(back); + + QPushButton *cont = new QPushButton("Continue"); + cont->setObjectName("navBtn"); + cont->setEnabled(false); + blayout->addWidget(cont); + + QObject::connect(cont, &QPushButton::clicked, [=]() { + QString url = DASHCAM_URL; + if (group->checkedButton() != dashcam) { + url = InputDialog::getText("Enter URL", this); + } + if (!url.isEmpty()) { + setCurrentWidget(downloading_widget); + QTimer::singleShot(100, this, [=]() { + download(url); + }); } }); - return build_page("Choose Software", widget, false, true); + + connect(group, QOverload::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *btn) { + btn->setChecked(true); + cont->setEnabled(true); + }); + + return widget; } QWidget * Setup::downloading() { QWidget *widget = new QWidget(); QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->addWidget(title_label("Downloading..."), 0, Qt::AlignCenter); + QLabel *txt = new QLabel("Downloading..."); + txt->setStyleSheet("font-size: 90px; font-weight: 500;"); + main_layout->addWidget(txt, 0, Qt::AlignCenter); return widget; } QWidget * Setup::download_failed() { QWidget *widget = new QWidget(); QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(50, 50, 50, 50); - main_layout->addWidget(title_label("Download Failed"), 0, Qt::AlignLeft | Qt::AlignTop); + main_layout->setContentsMargins(55, 225, 55, 55); + main_layout->setSpacing(0); + + QLabel *title = new QLabel("Download Failed"); + title->setStyleSheet("font-size: 90px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); + + main_layout->addSpacing(67); - QLabel *body = new QLabel("Ensure the entered URL is valid, and the device's network connection is good."); + QLabel *body = new QLabel("Ensure the entered URL is valid, and the device’s internet connection is good."); body->setWordWrap(true); - body->setAlignment(Qt::AlignHCenter); - body->setStyleSheet(R"(font-size: 80px;)"); + body->setAlignment(Qt::AlignTop | Qt::AlignLeft); + body->setStyleSheet("font-size: 80px; font-weight: 300; margin-right: 100px;"); main_layout->addWidget(body); - QHBoxLayout *nav_layout = new QHBoxLayout(); + main_layout->addStretch(); - QPushButton *reboot_btn = new QPushButton("Reboot"); - nav_layout->addWidget(reboot_btn, 0, Qt::AlignBottom | Qt::AlignLeft); - QObject::connect(reboot_btn, &QPushButton::released, this, [=]() { - if (Hardware::TICI()) { - std::system("sudo reboot"); - } + // reboot + start over buttons + QHBoxLayout *blayout = new QHBoxLayout(); + blayout->setSpacing(50); + main_layout->addLayout(blayout, 0); + + QPushButton *reboot = new QPushButton("Reboot device"); + reboot->setObjectName("navBtn"); + blayout->addWidget(reboot); + QObject::connect(reboot, &QPushButton::released, this, [=]() { + Hardware::reboot(); }); - QPushButton *restart_btn = new QPushButton("Start over"); - nav_layout->addWidget(restart_btn, 0, Qt::AlignBottom | Qt::AlignRight); - QObject::connect(restart_btn, &QPushButton::released, this, [=]() { + QPushButton *restart = new QPushButton("Start over"); + restart->setObjectName("navBtn"); + restart->setProperty("primary", true); + blayout->addWidget(restart); + QObject::connect(restart, &QPushButton::released, this, [=]() { setCurrentIndex(0); }); - main_layout->addLayout(nav_layout, 0); + widget->setStyleSheet(R"( + QLabel { + margin-left: 117; + } + )"); return widget; } @@ -168,24 +293,47 @@ Setup::Setup(QWidget *parent) : QStackedWidget(parent) { addWidget(getting_started()); addWidget(network_setup()); addWidget(software_selection()); - addWidget(downloading()); - addWidget(download_failed()); - QObject::connect(this, &Setup::downloadFailed, this, &Setup::nextPage); + downloading_widget = downloading(); + addWidget(downloading_widget); + + failed_widget = download_failed(); + addWidget(failed_widget); + QObject::connect(this, &Setup::finished, [=](bool success) { + // hide setup on success + qDebug() << "finished" << success; + setVisible(!success); + setCurrentWidget(failed_widget); + }); + + // TODO: revisit pressed bg color setStyleSheet(R"( * { - font-family: Inter; color: white; + font-family: Inter; + } + Setup { background-color: black; } - QPushButton { - padding: 50px; - padding-right: 100px; - padding-left: 100px; - border: 7px solid white; - border-radius: 20px; - font-size: 50px; + QPushButton#navBtn { + height: 160; + font-size: 55px; + font-weight: 400; + border-radius: 10px; + background-color: #333333; + } + QPushButton#navBtn:disabled { + color: #808080; + } + QPushButton#navBtn:pressed { + background-color: #444444; + } + QPushButton[primary='true'], #navBtn[primary='true'] { + background-color: #465BEA; + } + QPushButton[primary='true']:pressed, #navBtn:pressed[primary='true'] { + background-color: #3049F4; } )"); } diff --git a/selfdrive/ui/qt/setup/setup.h b/selfdrive/ui/qt/setup/setup.h index 92a8d98180..af5786da9b 100644 --- a/selfdrive/ui/qt/setup/setup.h +++ b/selfdrive/ui/qt/setup/setup.h @@ -14,14 +14,14 @@ private: QWidget *getting_started(); QWidget *network_setup(); QWidget *software_selection(); - QWidget *custom_software(); QWidget *downloading(); QWidget *download_failed(); - QWidget *build_page(QString title, QWidget *content, bool next, bool prev); + QWidget *failed_widget; + QWidget *downloading_widget; signals: - void downloadFailed(); + void finished(bool success); public slots: void nextPage();