diff --git a/selfdrive/hardware/tici/agnos.py b/selfdrive/hardware/tici/agnos.py index 24e544f23b..26b6d8f7b1 100755 --- a/selfdrive/hardware/tici/agnos.py +++ b/selfdrive/hardware/tici/agnos.py @@ -140,6 +140,7 @@ def flash_agnos_update(manifest_path, cloudlog, spinner=None): break except requests.exceptions.RequestException: + cloudlog.exception("Failed") spinner.update("Waiting for internet...") cloudlog.info(f"Failed to download {partition['name']}, retrying ({retries})") time.sleep(10) diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index a19fe1ee41..ffbcb3c858 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -18,8 +18,8 @@ else: qt_libs = qt_env["LIBS"] + libs + ["pthread", "ssl", "crypto"] widgets = qt_env.Library("qt_widgets", - ["qt/qt_window.cc", "qt/qt_sound.cc", "qt/widgets/keyboard.cc", "qt/widgets/input_field.cc", "qt/widgets/drive_stats.cc", - "qt/offroad/wifi.cc", "qt/offroad/wifiManager.cc", "qt/widgets/toggle.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/setup.cc"], + ["qt/qt_window.cc", "qt/qt_sound.cc", "qt/widgets/keyboard.cc", "qt/widgets/input_field.cc", "qt/widgets/drive_stats.cc", "qt/widgets/ssh_keys.cc", + "qt/offroad/networking.cc", "qt/offroad/wifiManager.cc", "qt/widgets/toggle.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/setup.cc"], LIBS=qt_libs) qt_libs.append(widgets) diff --git a/selfdrive/ui/qt/offroad/wifi.cc b/selfdrive/ui/qt/offroad/networking.cc similarity index 83% rename from selfdrive/ui/qt/offroad/wifi.cc rename to selfdrive/ui/qt/offroad/networking.cc index 741f0f0e05..3ab22ebf78 100644 --- a/selfdrive/ui/qt/offroad/wifi.cc +++ b/selfdrive/ui/qt/offroad/networking.cc @@ -6,8 +6,7 @@ #include #include -#include "wifi.hpp" -#include "widgets/toggle.hpp" +#include "networking.hpp" void clearLayout(QLayout* layout) { while (QLayoutItem* item = layout->takeAt(0)) { @@ -27,6 +26,20 @@ QWidget* layoutToWidget(QLayout* l, QWidget* parent){ return q; } +// https://stackoverflow.com/questions/478898/how-do-i-execute-a-command-and-get-the-output-of-the-command-within-c-using-po +std::string exec(const char* cmd) { + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; +} + // Networking functions Networking::Networking(QWidget* parent) : QWidget(parent){ @@ -179,7 +192,7 @@ QFrame* hline(QWidget* parent = 0){ // AdvancedNetworking functions AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWidget(parent), wifi(wifi){ - s = new QStackedLayout;// inputField and settings + s = new QStackedLayout;// inputField, mainPage, SSH settings inputField = new InputField(this, 8); connect(inputField, SIGNAL(emitText(QString)), this, SLOT(receiveText(QString))); connect(inputField, SIGNAL(cancel()), this, SLOT(abortTextInput())); @@ -191,7 +204,7 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid //Back button QHBoxLayout* backLayout = new QHBoxLayout; QPushButton* back = new QPushButton("BACK"); - back->setFixedWidth(500); + back->setFixedSize(500, 100); connect(back, &QPushButton::released, [=](){emit backPress();}); backLayout->addWidget(back, 0, Qt::AlignLeft); vlayout->addWidget(layoutToWidget(backLayout, this), 0, Qt::AlignLeft); @@ -222,28 +235,36 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid //IP adress QHBoxLayout* IPlayout = new QHBoxLayout; - IPlayout->addWidget(new QLabel("IP address: "), 0); + IPlayout->addWidget(new QLabel("IP address"), 0); ipLabel = new QLabel(wifi->ipv4_address); ipLabel->setStyleSheet("color: #aaaaaa"); IPlayout->addWidget(ipLabel, 0, Qt::AlignRight); vlayout->addWidget(layoutToWidget(IPlayout, this), 0); vlayout->addWidget(hline(), 0); - vlayout->addSpacing(300); - - // //Enable SSH - // QHBoxLayout* enableSSHLayout = new QHBoxLayout(this); - // enableSSHLayout->addWidget(new QLabel("Enable SSH", this)); - // Toggle* toggle_switch_SSH = new Toggle(this); - // toggle_switch_SSH->setFixedSize(150, 100); - // enableSSHLayout->addWidget(toggle_switch_SSH); - // vlayout->addWidget(layoutToWidget(enableSSHLayout, this)); - - // //Authorized SSH keys - // QHBoxLayout* authSSHLayout = new QHBoxLayout(this); - // authSSHLayout->addWidget(new QLabel("Authorized SSH keys", this)); - // QPushButton* editAuthSSHButton = new QPushButton("EDIT", this); - // authSSHLayout->addWidget(editAuthSSHButton); - // vlayout->addWidget(layoutToWidget(authSSHLayout, this)); + + //Enable SSH + QHBoxLayout* enableSSHLayout = new QHBoxLayout(this); + enableSSHLayout->addWidget(new QLabel("Enable SSH", this)); + toggle_switch_SSH = new Toggle(this); + toggle_switch_SSH->immediateOffset = 40; + toggle_switch_SSH->setFixedSize(150, 100); + if (isSSHEnabled()) { + toggle_switch_SSH->togglePosition(); + } + QObject::connect(toggle_switch_SSH, SIGNAL(stateChanged(int)), this, SLOT(toggleSSH(int))); + enableSSHLayout->addWidget(toggle_switch_SSH); + vlayout->addWidget(layoutToWidget(enableSSHLayout, this)); + vlayout->addWidget(hline(), 0); + + //Authorized SSH keys + QHBoxLayout* authSSHLayout = new QHBoxLayout(this); + authSSHLayout->addWidget(new QLabel("Authorized SSH keys", this)); + QPushButton* editAuthSSHButton = new QPushButton("EDIT", this); + editAuthSSHButton->setFixedWidth(500); + connect(editAuthSSHButton, &QPushButton::released, [=](){s->setCurrentIndex(2);}); + authSSHLayout->addWidget(editAuthSSHButton); + vlayout->addWidget(layoutToWidget(authSSHLayout, this)); + vlayout->addSpacing(50); // //Disconnect or delete connections // QHBoxLayout* dangerZone = new QHBoxLayout(this); @@ -258,11 +279,24 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid settingsWidget->setStyleSheet("margin-left: 40px; margin-right: 40px;"); s->addWidget(settingsWidget); s->setCurrentIndex(1); + + ssh = new SSH; + connect(ssh, &SSH::closeSSHSettings, [=](){s->setCurrentIndex(1);}); + s->addWidget(ssh); + setLayout(s); } +bool AdvancedNetworking::isSSHEnabled(){ + QString response = QString::fromStdString(exec("systemctl is-active ssh")); + return response.startsWith("active"); +} + void AdvancedNetworking::refresh(){ ipLabel->setText(wifi->ipv4_address); + if (toggle_switch_SSH->on != isSSHEnabled()) { + toggle_switch_SSH->togglePosition(); + } } void AdvancedNetworking::toggleTethering(int enable) { @@ -273,7 +307,16 @@ void AdvancedNetworking::toggleTethering(int enable) { } editPasswordButton->setEnabled(!enable); } +void AdvancedNetworking::toggleSSH(int enable) { + if (enable) { + system("sudo systemctl enable ssh"); + system("sudo systemctl start ssh"); + } else { + system("sudo systemctl stop ssh"); + system("sudo systemctl disable ssh"); + } +} void AdvancedNetworking::receiveText(QString text){ wifi->changeTetheringPassword(text); s->setCurrentIndex(1); diff --git a/selfdrive/ui/qt/offroad/wifi.hpp b/selfdrive/ui/qt/offroad/networking.hpp similarity index 92% rename from selfdrive/ui/qt/offroad/wifi.hpp rename to selfdrive/ui/qt/offroad/networking.hpp index 05fad4c306..8143fa05fa 100644 --- a/selfdrive/ui/qt/offroad/wifi.hpp +++ b/selfdrive/ui/qt/offroad/networking.hpp @@ -9,7 +9,8 @@ #include "wifiManager.hpp" #include "widgets/input_field.hpp" - +#include "widgets/ssh_keys.hpp" +#include "widgets/toggle.hpp" class WifiUI : public QWidget { Q_OBJECT @@ -49,10 +50,12 @@ private: InputField* inputField; QLabel* ipLabel; QPushButton* editPasswordButton; - + SSH* ssh; + Toggle* toggle_switch_SSH; WifiManager* wifi = nullptr; + bool isSSHEnabled(); signals: void openKeyboard(); void closeKeyboard(); @@ -62,6 +65,7 @@ public slots: void receiveText(QString text); void abortTextInput(); void toggleTethering(int enable); + void toggleSSH(int enable); void refresh(); }; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 060ce1fa1f..da4d7132ed 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -10,7 +10,7 @@ #include #include -#include "wifi.hpp" +#include "networking.hpp" #include "settings.hpp" #include "widgets/toggle.hpp" #include "widgets/offroad_alerts.hpp" @@ -145,7 +145,7 @@ QWidget * device_panel() { //} for (auto &l : labels) { - device_layout->addWidget(labelWidget(QString::fromStdString(l.first+":"), QString::fromStdString(l.second)), 0, Qt::AlignTop); + device_layout->addWidget(labelWidget(QString::fromStdString(l.first), QString::fromStdString(l.second)), 0, Qt::AlignTop); } // TODO: show current calibration values @@ -206,7 +206,7 @@ QWidget * developer_panel() { for (int i = 0; iaddWidget(labelWidget(QString::fromStdString(l.first+":"), QString::fromStdString(l.second))); + main_layout->addWidget(labelWidget(QString::fromStdString(l.first), QString::fromStdString(l.second))); if(i+1addWidget(horizontal_line()); diff --git a/selfdrive/ui/qt/offroad/settings.hpp b/selfdrive/ui/qt/offroad/settings.hpp index 5f4adf9f92..561c9c966c 100644 --- a/selfdrive/ui/qt/offroad/settings.hpp +++ b/selfdrive/ui/qt/offroad/settings.hpp @@ -7,7 +7,7 @@ #include #include -#include "wifi.hpp" +#include "networking.hpp" // *** settings widgets *** diff --git a/selfdrive/ui/qt/widgets/keyboard.cc b/selfdrive/ui/qt/widgets/keyboard.cc index 272dc716c4..75f655ca4d 100644 --- a/selfdrive/ui/qt/widgets/keyboard.cc +++ b/selfdrive/ui/qt/widgets/keyboard.cc @@ -81,7 +81,7 @@ Keyboard::Keyboard(QWidget *parent) : QWidget(parent) { // Special characters std::vector> specials = { {"[","]","{","}","#","%","^","*","+","="}, - {"_","\\","|","~","<",">","€","£","¥"," "}, + {"_","\\","|","~","<",">","€","£","¥","•"}, {"123",".",",","?","!","`","⌫"}, {"ABC"," ","⏎"}, }; diff --git a/selfdrive/ui/qt/widgets/ssh_keys.cc b/selfdrive/ui/qt/widgets/ssh_keys.cc new file mode 100644 index 0000000000..f4b58d645f --- /dev/null +++ b/selfdrive/ui/qt/widgets/ssh_keys.cc @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "widgets/ssh_keys.hpp" +#include "widgets/input_field.hpp" +#include "common/params.h" + +QWidget* layout_to_widget(QLayout* l){ + QWidget* q = new QWidget; + q->setLayout(l); + return q; +} + +SSH::SSH(QWidget* parent) : QWidget(parent){ + // init variables + manager = new QNetworkAccessManager(this); + networkTimer = new QTimer(this); + networkTimer->setSingleShot(true); + networkTimer->setInterval(5000); + connect(networkTimer, SIGNAL(timeout()), this, SLOT(timeout())); + + + // Construct the layouts to display + slayout = new QStackedLayout(this); // Initial screen, input, waiting for response + + //Layout on entering + QVBoxLayout* initialLayout = new QVBoxLayout; + initialLayout->setContentsMargins(80, 80, 80, 80); + + QHBoxLayout* header = new QHBoxLayout; + QPushButton* exitButton = new QPushButton("BACK", this); + exitButton->setFixedSize(500, 100); + header->addWidget(exitButton, 0, Qt::AlignLeft | Qt::AlignTop); + initialLayout->addWidget(layout_to_widget(header)); + + QLabel* title = new QLabel("Authorize SSH keys"); + title->setStyleSheet(R"(font-size: 75px;)"); + header->addWidget(title, 0, Qt::AlignRight | Qt::AlignTop); + + QLabel* wallOfText = new QLabel("Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A Comma employee will NEVER ask you to add their GitHub username."); + wallOfText->setWordWrap(true); + wallOfText->setStyleSheet(R"(font-size: 60px;)"); + initialLayout->addWidget(wallOfText, 0); + + QPushButton* actionButton = new QPushButton; + actionButton->setFixedHeight(100); + initialLayout->addWidget(actionButton, 0, Qt::AlignBottom); + + slayout->addWidget(layout_to_widget(initialLayout)); + + InputField* input = new InputField; + slayout->addWidget(input); + + QLabel* loading = new QLabel("Loading SSH keys from GitHub."); + slayout->addWidget(loading); + setStyleSheet(R"( + QPushButton { + font-size: 60px; + margin: 0px; + padding: 15px; + border-radius: 25px; + color: #dddddd; + background-color: #444444; + } + )"); + setLayout(slayout); + + + //Initialize the state machine and states + QStateMachine* state = new QStateMachine(this); + QState* initialState = new QState(); //State when entering the widget + QState* initialStateNoGithub = new QState(); //Starting state, key not connected + QState* initialStateConnected = new QState(); //Starting state, ssh connected + QState* quitState = new QState(); // State when exiting the widget + QState* removeSSH_State = new QState(); // State when user wants to remove the SSH keys + QState* defaultInputFieldState = new QState(); // State when we want the user to give us the username + QState* loadingState = new QState(); // State while waiting for the network response + + + // Adding states to the state machine and adding the transitions + state->addState(initialState); + connect(initialState, &QState::entered, [=](){checkForSSHKey(); slayout->setCurrentIndex(0);}); + initialState->addTransition(this, &SSH::NoSSHAdded, initialStateNoGithub); + initialState->addTransition(this, &SSH::SSHAdded, initialStateConnected); + + + state->addState(quitState); + connect(quitState, &QState::entered, [=](){emit closeSSHSettings();}); + quitState->addTransition(quitState, &QState::entered, initialState); + + state->addState(initialStateConnected); + connect(initialStateConnected, &QState::entered, [=](){actionButton->setText("Remove GitHub SSH keys"); actionButton->setStyleSheet(R"(background-color: #750c0c;)");}); + initialStateConnected->addTransition(exitButton, &QPushButton::released, quitState); + initialStateConnected->addTransition(actionButton, &QPushButton::released, removeSSH_State); + + state->addState(removeSSH_State); + connect(removeSSH_State, &QState::entered, [=](){Params().delete_db_value("GithubSshKeys");}); + removeSSH_State->addTransition(removeSSH_State, &QState::entered, initialState); + + state->addState(initialStateNoGithub); + connect(initialStateNoGithub, &QState::entered, [=](){actionButton->setText("Link GitHub SSH keys"); actionButton->setStyleSheet(R"(background-color: #444444;)");}); + initialStateNoGithub->addTransition(exitButton, &QPushButton::released, quitState); + initialStateNoGithub->addTransition(actionButton, &QPushButton::released, defaultInputFieldState); + connect(actionButton, &QPushButton::released, [=](){input->setPromptText("Enter your GitHub username");}); + + state->addState(defaultInputFieldState); + connect(defaultInputFieldState, &QState::entered, [=](){slayout->setCurrentIndex(1);}); + connect(input, &InputField::emitText, [=](QString a){usernameGitHub = a;}); // Store the string the user provided + defaultInputFieldState->addTransition(input, &InputField::cancel, initialState); + defaultInputFieldState->addTransition(input, &InputField::emitText, loadingState); + + state->addState(loadingState); + connect(loadingState, &QState::entered, [=](){slayout->setCurrentIndex(2); getSSHKeys();}); + connect(this, &SSH::failedResponse, [=](QString message){input->setPromptText(message);}); + loadingState->addTransition(this, &SSH::failedResponse, defaultInputFieldState); + loadingState->addTransition(this, &SSH::gotSSHKeys, initialState); + + + state->setInitialState(initialState); + state->start(); +} + +void SSH::checkForSSHKey(){ + QString SSHKey = QString::fromStdString(Params().get("GithubSshKeys")); + if (SSHKey.length()) { + emit SSHAdded(); + } else { + emit NoSSHAdded(); + } +} + +void SSH::getSSHKeys(){ + QString url = "https://github.com/" + usernameGitHub + ".keys"; + aborted = false; + reply = manager->get(QNetworkRequest(QUrl(url))); + connect(reply, SIGNAL(finished()), this, SLOT(parseResponse())); + networkTimer->start(); +} + +void SSH::timeout(){ + aborted = true; + reply->abort(); +} + +void SSH::parseResponse(){ + if (!aborted) { + networkTimer->stop(); + QString response = reply->readAll(); + if (reply->error() == QNetworkReply::NoError && response.length()) { + Params().write_db_value("GithubSshKeys", response.toStdString()); + emit gotSSHKeys(); + } else { + emit failedResponse("Username "+usernameGitHub+" doesn't exist"); + } + }else{ + emit failedResponse("Request timed out"); + } + reply->deleteLater(); + reply = NULL; +} diff --git a/selfdrive/ui/qt/widgets/ssh_keys.hpp b/selfdrive/ui/qt/widgets/ssh_keys.hpp new file mode 100644 index 0000000000..96d6a5d156 --- /dev/null +++ b/selfdrive/ui/qt/widgets/ssh_keys.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "widgets/input_field.hpp" + +class SSH : public QWidget { + Q_OBJECT + +public: + explicit SSH(QWidget* parent = 0); + +private: + InputField* inputField; + QStackedLayout* slayout; + QString usernameGitHub; + QNetworkAccessManager* manager; + QNetworkReply* reply; + QTimer* networkTimer; + bool aborted; + +signals: + void closeSSHSettings(); + void openKeyboard(); + void closeKeyboard(); + void NoSSHAdded(); + void SSHAdded(); + void failedResponse(QString errorString); + void gotSSHKeys(); + +private slots: + void checkForSSHKey(); + void getSSHKeys(); + void timeout(); + void parseResponse(); +}; + diff --git a/selfdrive/ui/qt/widgets/toggle.cc b/selfdrive/ui/qt/widgets/toggle.cc index f332a4772e..76241fcbd0 100644 --- a/selfdrive/ui/qt/widgets/toggle.cc +++ b/selfdrive/ui/qt/widgets/toggle.cc @@ -3,7 +3,7 @@ Toggle::Toggle(QWidget *parent) : QAbstractButton(parent), _height(80), _height_rect(60), -_on(false), +on(false), _anim(new QPropertyAnimation(this, "offset_circle", this)) { _radius = _height / 2; @@ -32,20 +32,27 @@ void Toggle::paintEvent(QPaintEvent *e) { } void Toggle::mouseReleaseEvent(QMouseEvent *e) { + const int left = _radius; + const int right = width() - _radius; + if(_x_circle != left && _x_circle != right){ + //Don't parse touch events, while the animation is running + return; + } if (e->button() & Qt::LeftButton) { togglePosition(); - emit stateChanged(_on); + emit stateChanged(on); } } void Toggle::togglePosition() { - _on = !_on; + on = !on; const int left = _radius; const int right = width() - _radius; - _anim->setStartValue(_on ? left : right); - _anim->setEndValue(_on ? right : left); - _anim->setDuration(120); + _anim->setStartValue(on ? left + immediateOffset : right - immediateOffset); + _anim->setEndValue(on ? right : left); + _anim->setDuration(animation_duration); _anim->start(); + repaint(); } void Toggle::enterEvent(QEvent *e) { diff --git a/selfdrive/ui/qt/widgets/toggle.hpp b/selfdrive/ui/qt/widgets/toggle.hpp index ca73f47e17..ad2e158ba8 100644 --- a/selfdrive/ui/qt/widgets/toggle.hpp +++ b/selfdrive/ui/qt/widgets/toggle.hpp @@ -8,7 +8,9 @@ class Toggle : public QAbstractButton { public: Toggle(QWidget* parent = nullptr); void togglePosition(); - + bool on; + int animation_duration = 250; + int immediateOffset = 0; int offset_circle() const { return _x_circle; } @@ -18,13 +20,13 @@ public: update(); } + protected: void paintEvent(QPaintEvent*) override; void mouseReleaseEvent(QMouseEvent*) override; void enterEvent(QEvent*) override; private: - bool _on; int _x_circle, _y_circle; int _height, _radius; int _height_rect, _y_rect;