diff --git a/common/params.cc b/common/params.cc index 5c8c94be53..8ff5fc539d 100644 --- a/common/params.cc +++ b/common/params.cc @@ -154,18 +154,24 @@ std::unordered_map keys = { {"PrimeType", PERSISTENT}, {"RecordFront", PERSISTENT}, {"RecordFrontLock", PERSISTENT}, // for the internal fleet - {"ReleaseNotes", PERSISTENT}, {"ReplayControlsState", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"ShouldDoUpdate", CLEAR_ON_MANAGER_START}, {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, {"SshEnabled", PERSISTENT}, {"SubscriberInfo", PERSISTENT}, - {"SwitchToBranch", CLEAR_ON_MANAGER_START}, {"TermsVersion", PERSISTENT}, {"Timezone", PERSISTENT}, {"TrainingVersion", PERSISTENT}, {"UpdateAvailable", CLEAR_ON_MANAGER_START}, {"UpdateFailedCount", CLEAR_ON_MANAGER_START}, + {"UpdaterState", CLEAR_ON_MANAGER_START}, + {"UpdaterFetchAvailable", CLEAR_ON_MANAGER_START}, + {"UpdaterTargetBranch", CLEAR_ON_MANAGER_START}, + {"UpdaterAvailableBranches", CLEAR_ON_MANAGER_START}, + {"UpdaterCurrentDescription", CLEAR_ON_MANAGER_START}, + {"UpdaterCurrentReleaseNotes", CLEAR_ON_MANAGER_START}, + {"UpdaterNewDescription", CLEAR_ON_MANAGER_START}, + {"UpdaterNewReleaseNotes", CLEAR_ON_MANAGER_START}, {"Version", PERSISTENT}, {"VisionRadarToggle", PERSISTENT}, {"WideCameraOnly", PERSISTENT}, diff --git a/selfdrive/ui/.gitignore b/selfdrive/ui/.gitignore index e5b27adce5..60eb4b43c7 100644 --- a/selfdrive/ui/.gitignore +++ b/selfdrive/ui/.gitignore @@ -1,6 +1,8 @@ moc_* *.moc +translations/main_test_en.* + _mui watch3 installer/installers/* diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index c62a6b19d9..92f6578dfc 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -56,7 +56,8 @@ qt_env.Program("qt/spinner", ["qt/spinner.cc"], LIBS=qt_libs) # build main UI 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/onboarding.cc", "qt/offroad/driverview.cc"] + "qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc", + "qt/offroad/driverview.cc"] qt_env.Program("_ui", qt_src + [asset_obj], LIBS=qt_libs) if GetOption('test'): qt_src.remove("main.cc") # replaced by test_runner diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 7a5a40c193..52df247a25 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -159,7 +159,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { addItem(dcamBtn); auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); - connect(resetCalibBtn, &ButtonControl::showDescription, this, &DevicePanel::updateCalibDescription); + connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); connect(resetCalibBtn, &ButtonControl::clicked, [&]() { if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), this)) { params.remove("CalibrationParams"); @@ -282,85 +282,6 @@ void DevicePanel::poweroff() { } } -SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { - gitBranchLbl = new LabelControl(tr("Git Branch")); - gitCommitLbl = new LabelControl(tr("Git Commit")); - osVersionLbl = new LabelControl(tr("OS Version")); - versionLbl = new LabelControl(tr("Version"), "", QString::fromStdString(params.get("ReleaseNotes")).trimmed()); - lastUpdateLbl = new LabelControl(tr("Last Update Check"), "", tr("The last time openpilot successfully checked for an update. The updater only runs while the car is off.")); - updateBtn = new ButtonControl(tr("Check for Update"), ""); - connect(updateBtn, &ButtonControl::clicked, [=]() { - if (params.getBool("IsOffroad")) { - fs_watch->addPath(QString::fromStdString(params.getParamPath("LastUpdateTime"))); - fs_watch->addPath(QString::fromStdString(params.getParamPath("UpdateFailedCount"))); - updateBtn->setText(tr("CHECKING")); - updateBtn->setEnabled(false); - } - std::system("pkill -1 -f selfdrive.updated"); - }); - connect(uiState(), &UIState::offroadTransition, updateBtn, &QPushButton::setEnabled); - - branchSwitcherBtn = new ButtonControl(tr("Switch Branch"), tr("ENTER"), tr("The new branch will be pulled the next time the updater runs.")); - connect(branchSwitcherBtn, &ButtonControl::clicked, [=]() { - QString branch = InputDialog::getText(tr("Enter branch name"), this, tr("The new branch will be pulled the next time the updater runs."), - false, -1, QString::fromStdString(params.get("SwitchToBranch"))); - if (branch.isEmpty()) { - params.remove("SwitchToBranch"); - } else { - params.put("SwitchToBranch", branch.toStdString()); - } - std::system("pkill -1 -f selfdrive.updated"); - }); - connect(uiState(), &UIState::offroadTransition, branchSwitcherBtn, &QPushButton::setEnabled); - - auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); - connect(uninstallBtn, &ButtonControl::clicked, [&]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), this)) { - params.putBool("DoUninstall", true); - } - }); - connect(uiState(), &UIState::offroadTransition, uninstallBtn, &QPushButton::setEnabled); - - QWidget *widgets[] = {versionLbl, lastUpdateLbl, updateBtn, branchSwitcherBtn, gitBranchLbl, gitCommitLbl, osVersionLbl, uninstallBtn}; - for (QWidget* w : widgets) { - if (w == branchSwitcherBtn && params.getBool("IsTestedBranch")) { - continue; - } - addItem(w); - } - - fs_watch = new QFileSystemWatcher(this); - QObject::connect(fs_watch, &QFileSystemWatcher::fileChanged, [=](const QString path) { - if (path.contains("UpdateFailedCount") && std::atoi(params.get("UpdateFailedCount").c_str()) > 0) { - lastUpdateLbl->setText(tr("failed to fetch update")); - updateBtn->setText(tr("CHECK")); - updateBtn->setEnabled(true); - } else if (path.contains("LastUpdateTime")) { - updateLabels(); - } - }); -} - -void SoftwarePanel::showEvent(QShowEvent *event) { - updateLabels(); -} - -void SoftwarePanel::updateLabels() { - QString lastUpdate = ""; - auto tm = params.get("LastUpdateTime"); - if (!tm.empty()) { - lastUpdate = timeAgo(QDateTime::fromString(QString::fromStdString(tm + "Z"), Qt::ISODate)); - } - - versionLbl->setText(getBrandVersion()); - lastUpdateLbl->setText(lastUpdate); - updateBtn->setText(tr("CHECK")); - updateBtn->setEnabled(true); - gitBranchLbl->setText(QString::fromStdString(params.get("GitBranch"))); - gitCommitLbl->setText(QString::fromStdString(params.get("GitCommit")).left(10)); - osVersionLbl->setText(QString::fromStdString(Hardware::get_os_version()).trimmed()); -} - void SettingsWindow::showEvent(QShowEvent *event) { panel_widget->setCurrentIndex(0); nav_btns->buttons()[0]->setChecked(true); diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 1f823851f1..4177f28cf4 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -71,14 +71,15 @@ public: private: void showEvent(QShowEvent *event) override; void updateLabels(); + void checkForUpdates(); - LabelControl *gitBranchLbl; - LabelControl *gitCommitLbl; - LabelControl *osVersionLbl; + bool is_onroad = false; + + QLabel *onroadLbl; LabelControl *versionLbl; - LabelControl *lastUpdateLbl; - ButtonControl *updateBtn; - ButtonControl *branchSwitcherBtn; + ButtonControl *installBtn; + ButtonControl *downloadBtn; + ButtonControl *targetBranchBtn; Params params; QFileSystemWatcher *fs_watch; diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc new file mode 100644 index 0000000000..c9deef2ec4 --- /dev/null +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -0,0 +1,156 @@ +#include "selfdrive/ui/qt/offroad/settings.h" + +#include +#include +#include + +#include +#include + +#include "common/params.h" +#include "common/util.h" +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "system/hardware/hw.h" + + +void SoftwarePanel::checkForUpdates() { + std::system("pkill -SIGUSR1 -f selfdrive.updated"); +} + +SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { + onroadLbl = new QLabel(tr("Updates are only downloaded while the car is off.")); + onroadLbl->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; padding-top: 30px; padding-bottom: 30px;"); + addItem(onroadLbl); + + // current version + versionLbl = new LabelControl(tr("Current Version"), ""); + addItem(versionLbl); + + // download update btn + downloadBtn = new ButtonControl(tr("Download"), tr("CHECK")); + connect(downloadBtn, &ButtonControl::clicked, [=]() { + downloadBtn->setEnabled(false); + if (downloadBtn->text() == tr("CHECK")) { + checkForUpdates(); + } else { + std::system("pkill -SIGHUP -f selfdrive.updated"); + } + }); + addItem(downloadBtn); + + // install update btn + installBtn = new ButtonControl(tr("Install Update"), tr("INSTALL")); + connect(installBtn, &ButtonControl::clicked, [=]() { + installBtn->setEnabled(false); + params.putBool("DoShutdown", true); + }); + addItem(installBtn); + + // branch selecting + targetBranchBtn = new ButtonControl(tr("Target Branch"), tr("SELECT")); + connect(targetBranchBtn, &ButtonControl::clicked, [=]() { + auto current = params.get("GitBranch"); + QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); + for (QString b : {current.c_str(), "devel-staging", "devel", "master-ci", "master"}) { + auto i = branches.indexOf(b); + if (i >= 0) { + branches.removeAt(i); + branches.insert(0, b); + } + } + + QString cur = QString::fromStdString(params.get("UpdaterTargetBranch")); + QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this); + if (!selection.isEmpty()) { + params.put("UpdaterTargetBranch", selection.toStdString()); + targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); + checkForUpdates(); + } + }); + if (!params.getBool("IsTestedBranch")) { + addItem(targetBranchBtn); + } + + // uninstall button + auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); + connect(uninstallBtn, &ButtonControl::clicked, [&]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), this)) { + params.putBool("DoUninstall", true); + } + }); + addItem(uninstallBtn); + + fs_watch = new QFileSystemWatcher(this); + QObject::connect(fs_watch, &QFileSystemWatcher::fileChanged, [=](const QString path) { + updateLabels(); + }); + + connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + is_onroad = !offroad; + updateLabels(); + }); + + updateLabels(); +} + +void SoftwarePanel::showEvent(QShowEvent *event) { + // nice for testing on PC + installBtn->setEnabled(true); + + updateLabels(); +} + +void SoftwarePanel::updateLabels() { + // add these back in case the files got removed + fs_watch->addPath(QString::fromStdString(params.getParamPath("LastUpdateTime"))); + fs_watch->addPath(QString::fromStdString(params.getParamPath("UpdateFailedCount"))); + fs_watch->addPath(QString::fromStdString(params.getParamPath("UpdaterState"))); + fs_watch->addPath(QString::fromStdString(params.getParamPath("UpdateAvailable"))); + + if (!isVisible()) { + return; + } + + // updater only runs offroad + onroadLbl->setVisible(is_onroad); + downloadBtn->setVisible(!is_onroad); + + // download update + QString updater_state = QString::fromStdString(params.get("UpdaterState")); + bool failed = std::atoi(params.get("UpdateFailedCount").c_str()) > 0; + if (updater_state != "idle") { + downloadBtn->setEnabled(false); + downloadBtn->setValue(updater_state); + } else { + if (failed) { + downloadBtn->setText("CHECK"); + downloadBtn->setValue("failed to check for update"); + } else if (params.getBool("UpdaterFetchAvailable")) { + downloadBtn->setText("DOWNLOAD"); + downloadBtn->setValue("update available"); + } else { + QString lastUpdate = "never"; + auto tm = params.get("LastUpdateTime"); + if (!tm.empty()) { + lastUpdate = timeAgo(QDateTime::fromString(QString::fromStdString(tm + "Z"), Qt::ISODate)); + } + downloadBtn->setText("CHECK"); + downloadBtn->setValue("up to date, last checked " + lastUpdate); + } + downloadBtn->setEnabled(true); + } + targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); + + // current + new versions + versionLbl->setText(QString::fromStdString(params.get("UpdaterCurrentDescription")).left(40)); + versionLbl->setDescription(QString::fromStdString(params.get("UpdaterCurrentReleaseNotes"))); + + installBtn->setVisible(!is_onroad && params.getBool("UpdateAvailable")); + installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription")).left(35)); + installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes"))); + + update(); +} diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 3264fd3aac..b5f646c379 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -42,6 +42,12 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons title_label->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left"); hlayout->addWidget(title_label); + // value next to control button + value = new QLabel(); + value->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + value->setStyleSheet("color: #aaaaaa"); + hlayout->addWidget(value); + main_layout->addLayout(hlayout); // description @@ -54,7 +60,7 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons connect(title_label, &QPushButton::clicked, [=]() { if (!description->isVisible()) { - emit showDescription(); + emit showDescriptionEvent(); } if (!description->text().isEmpty()) { diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index d8546bb3b5..c42716828f 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -45,8 +45,17 @@ public: title_label->setText(title); } + void setValue(const QString &val) { + value->setText(val); + } + +public slots: + void showDescription() { + description->setVisible(true); + }; + signals: - void showDescription(); + void showDescriptionEvent(); protected: AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); @@ -54,6 +63,9 @@ protected: QHBoxLayout *hlayout; QPushButton *title_label; + +private: + QLabel *value; QLabel *description = nullptr; }; diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.cc b/selfdrive/ui/qt/widgets/offroad_alerts.cc index 937ea02f86..ceb823fb2b 100644 --- a/selfdrive/ui/qt/widgets/offroad_alerts.cc +++ b/selfdrive/ui/qt/widgets/offroad_alerts.cc @@ -112,7 +112,7 @@ UpdateAlert::UpdateAlert(QWidget *parent) : AbstractAlert(true, parent) { bool UpdateAlert::refresh() { bool updateAvailable = params.getBool("UpdateAvailable"); if (updateAvailable) { - releaseNotes->setText(params.get("ReleaseNotes").c_str()); + releaseNotes->setText(params.get("UpdaterNewReleaseNotes").c_str()); } return updateAvailable; } diff --git a/selfdrive/ui/qt/widgets/ssh_keys.cc b/selfdrive/ui/qt/widgets/ssh_keys.cc index f17604b3e5..1097a89268 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.cc +++ b/selfdrive/ui/qt/widgets/ssh_keys.cc @@ -5,10 +5,6 @@ #include "selfdrive/ui/qt/widgets/input.h" SshControl::SshControl() : ButtonControl(tr("SSH Keys"), "", tr("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.")) { - username_label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); - username_label.setStyleSheet("color: #aaaaaa"); - hlayout->insertWidget(1, &username_label); - QObject::connect(this, &ButtonControl::clicked, [=]() { if (text() == tr("ADD")) { QString username = InputDialog::getText(tr("Enter your GitHub username"), this); @@ -30,10 +26,10 @@ SshControl::SshControl() : ButtonControl(tr("SSH Keys"), "", tr("Warning: This g void SshControl::refresh() { QString param = QString::fromStdString(params.get("GithubSshKeys")); if (param.length()) { - username_label.setText(QString::fromStdString(params.get("GithubUsername"))); + setValue(QString::fromStdString(params.get("GithubUsername"))); setText(tr("REMOVE")); } else { - username_label.setText(""); + setValue(""); setText(tr("ADD")); } setEnabled(true); diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h index 01e2ab83ce..920bd651e2 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.h +++ b/selfdrive/ui/qt/widgets/ssh_keys.h @@ -27,8 +27,6 @@ public: private: Params params; - QLabel username_label; - void refresh(); void getUserKeys(const QString &username); }; diff --git a/selfdrive/ui/tests/cycle_offroad_alerts.py b/selfdrive/ui/tests/cycle_offroad_alerts.py index 6b6aea4477..8a3d9ec45a 100755 --- a/selfdrive/ui/tests/cycle_offroad_alerts.py +++ b/selfdrive/ui/tests/cycle_offroad_alerts.py @@ -20,7 +20,7 @@ if __name__ == "__main__": params.put_bool("UpdateAvailable", True) r = open(os.path.join(BASEDIR, "RELEASES.md")).read() r = r[:r.find('\n\n')] # Slice latest release notes - params.put("ReleaseNotes", r + "\n") + params.put("UpdaterNewReleaseNotes", r + "\n") time.sleep(t) params.put_bool("UpdateAvailable", False) diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 9be6658d19..4262473fb2 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -193,7 +193,7 @@ 変更 - + Select a language 言語を選択 @@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai 詳しくはこちら:https://connect.comma.ai - + No home location set 自宅の住所はまだ @@ -432,7 +432,7 @@ location set 設定されていません - + no recent destinations 最近の目的地履歴がありません @@ -718,7 +718,7 @@ location set SettingsWindow - + × × @@ -983,68 +983,47 @@ location set SoftwarePanel - - Git Branch - Git ブランチ + + Updates are only downloaded while the car is off. + - - Git Commit - Git コミット - - - - OS Version - OS バージョン + + Current Version + - - Version - バージョン + + Download + - - Last Update Check - 最終更新確認 + + Install Update + - The last time openpilot successfully checked for an update. The updater only runs while the car is off. - openpilotが最後にアップデートの確認に成功してからの時間です。アップデート処理は、車の電源が切れているときのみ実行されます。 - - - - Check for Update - 更新プログラムをチェック - - - - CHECKING - 確認中 + INSTALL + - - Switch Branch - ブランチの切り替え + + Target Branch + - ENTER - 切替 + SELECT + - - - The new branch will be pulled the next time the updater runs. - updater を実行する時にブランチを切り替えます。 - - - - Enter branch name - ブランチ名を入力 + + Select a branch + - + UNINSTALL アンインストール @@ -1059,13 +1038,8 @@ location set アンインストールしてもよろしいですか? - - failed to fetch update - 更新のダウンロードにエラーが発生しました - - - - + + CHECK 確認 @@ -1083,7 +1057,7 @@ location set 警告: これは、GitHub の設定にあるすべての公開鍵への SSH アクセスを許可するものです。自分以外の GitHub のユーザー名を入力しないでください。コンマのスタッフが GitHub のユーザー名を追加するようお願いすることはありません。 - + ADD 追加 @@ -1153,7 +1127,7 @@ location set TogglesPanel - + Enable openpilot openpilot を有効化 @@ -1290,12 +1264,11 @@ location set WifiUI - Scanning for networks... ネットワークをスキャン中... - + CONNECTING... 接続中... diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 3e329a72ab..b517c8320e 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -193,7 +193,7 @@ 변경 - + Select a language 언어를 선택하세요 @@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai 등록:https://connect.comma.ai - + No home location set 집 @@ -432,7 +432,7 @@ location set 설정되지않음 - + no recent destinations 최근 목적지 없음 @@ -718,7 +718,7 @@ location set SettingsWindow - + × × @@ -983,68 +983,47 @@ location set SoftwarePanel - - Git Branch - Git 브렌치 + + Updates are only downloaded while the car is off. + - - Git Commit - Git 커밋 - - - - OS Version - OS 버전 + + Current Version + - - Version - 버전 + + Download + - - Last Update Check - 최신 업데이트 검사 + + Install Update + - The last time openpilot successfully checked for an update. The updater only runs while the car is off. - 최근에 openpilot이 업데이트를 성공적으로 확인했습니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. - - - - Check for Update - 업데이트 확인 - - - - CHECKING - 확인중 + INSTALL + - - Switch Branch - 브랜치 변경 + + Target Branch + - ENTER - 입력하세요 + SELECT + - - - The new branch will be pulled the next time the updater runs. - 다음 업데이트 프로그램이 실행될 때 새 브랜치가 적용됩니다. - - - - Enter branch name - 브랜치명 입력 + + Select a branch + - + UNINSTALL 제거 @@ -1059,13 +1038,8 @@ location set 제거하시겠습니까? - - failed to fetch update - 업데이트를 가져올수없습니다 - - - - + + CHECK 확인 @@ -1083,7 +1057,7 @@ location set 경고: 허용으로 설정하면 GitHub 설정의 모든 공용 키에 대한 SSH 액세스 권한이 부여됩니다. GitHub 사용자 ID 이외에는 입력하지 마십시오. comma에서는 GitHub ID를 추가하라는 요청을 하지 않습니다. - + ADD 추가 @@ -1153,7 +1127,7 @@ location set TogglesPanel - + Enable openpilot openpilot 사용 @@ -1290,12 +1264,11 @@ location set WifiUI - Scanning for networks... 네트워크 검색 중... - + CONNECTING... 연결중... diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index b729678a2c..91ffabc625 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -193,7 +193,7 @@ ALTERAR - + Select a language Selecione o Idioma @@ -419,7 +419,7 @@ prime subscription. Sign up now: https://connect.comma.ai uma assinatura prime Inscreva-se agora: https://connect.comma.ai - + No home location set Sem local @@ -433,7 +433,7 @@ location set trabalho definido - + no recent destinations sem destinos recentes @@ -722,7 +722,7 @@ trabalho definido SettingsWindow - + × × @@ -987,68 +987,47 @@ trabalho definido SoftwarePanel - - Git Branch - Git Branch - - - - Git Commit - Último Commit + + Updates are only downloaded while the car is off. + - - OS Version - Versão do Sistema + + Current Version + - - Version - Versão + + Download + - - Last Update Check - Verificação da última atualização + + Install Update + - The last time openpilot successfully checked for an update. The updater only runs while the car is off. - A última vez que o openpilot verificou com sucesso uma atualização. O atualizador só funciona com o carro desligado. - - - - Check for Update - Verifique atualizações - - - - CHECKING - VERIFICANDO - - - - Switch Branch - Alterar Branch + INSTALL + - - ENTER - INSERIR + + Target Branch + - - The new branch will be pulled the next time the updater runs. - A nova branch será aplicada ao verificar atualizações. + SELECT + - - Enter branch name - Inserir o nome da branch + + Select a branch + - + UNINSTALL DESINSTALAR @@ -1063,13 +1042,8 @@ trabalho definido Tem certeza que quer desinstalar? - - failed to fetch update - falha ao buscar atualização - - - - + + CHECK VERIFICAR @@ -1087,7 +1061,7 @@ trabalho definido Aviso: isso concede acesso SSH a todas as chaves públicas nas configurações do GitHub. Nunca insira um nome de usuário do GitHub que não seja o seu. Um funcionário da comma NUNCA pedirá que você adicione seu nome de usuário do GitHub. - + ADD ADICIONAR @@ -1157,7 +1131,7 @@ trabalho definido TogglesPanel - + Enable openpilot Ativar openpilot @@ -1294,12 +1268,11 @@ trabalho definido WifiUI - Scanning for networks... Procurando redes... - + CONNECTING... CONECTANDO... diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 606e70726f..8fce4e01a7 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -193,7 +193,7 @@ 切换 - + Select a language 选择语言 @@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai 立即注册:https://connect.comma.ai - + No home location set 家:未设定 @@ -430,7 +430,7 @@ location set 工作:未设定 - + no recent destinations 无最近目的地 @@ -716,7 +716,7 @@ location set SettingsWindow - + × × @@ -981,68 +981,47 @@ location set SoftwarePanel - - Git Branch - Git Branch - - - - Git Commit - Git Commit + + Updates are only downloaded while the car is off. + - - OS Version - 系统版本 + + Current Version + - - Version - 软件版本 + + Download + - - Last Update Check - 上次检查更新 + + Install Update + - The last time openpilot successfully checked for an update. The updater only runs while the car is off. - 上一次成功检查更新的时间。更新程序仅在汽车熄火时运行。 - - - - Check for Update - 检查更新 - - - - CHECKING - 正在检查更新 - - - - Switch Branch - 切换分支 + INSTALL + - - ENTER - 输入 + + Target Branch + - - The new branch will be pulled the next time the updater runs. - 分支将在更新服务下次启动时自动切换。 + SELECT + - - Enter branch name - 输入分支名称 + + Select a branch + - + UNINSTALL 卸载 @@ -1057,13 +1036,8 @@ location set 您确定要卸载吗? - - failed to fetch update - 获取更新失败 - - - - + + CHECK 查看 @@ -1081,7 +1055,7 @@ location set 警告:这将授予SSH访问权限给您GitHub设置中的所有公钥。切勿输入您自己以外的GitHub用户名。comma员工永远不会要求您添加他们的GitHub用户名。 - + ADD 添加 @@ -1151,7 +1125,7 @@ location set TogglesPanel - + Enable openpilot 启用openpilot @@ -1288,12 +1262,11 @@ location set WifiUI - Scanning for networks... 正在扫描网络…… - + CONNECTING... 正在连接…… diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index b5b737ca25..022d41be81 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -193,7 +193,7 @@ 更改 - + Select a language 選擇語言 @@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai 立即註冊:https://connect.comma.ai - + No home location set 未設定 @@ -432,7 +432,7 @@ location set 工作位置 - + no recent destinations 沒有最近的導航記錄 @@ -718,7 +718,7 @@ location set SettingsWindow - + × × @@ -983,68 +983,47 @@ location set SoftwarePanel - - Git Branch - Git 分支 - - - - Git Commit - Git 提交 + + Updates are only downloaded while the car is off. + - - OS Version - 系統版本 + + Current Version + - - Version - 版本 + + Download + - - Last Update Check - 上次檢查時間 + + Install Update + - The last time openpilot successfully checked for an update. The updater only runs while the car is off. - 上次成功檢查更新的時間。更新系統只會在車子熄火時執行。 - - - - Check for Update - 檢查更新 - - - - CHECKING - 檢查中 - - - - Switch Branch - 切換分支 + INSTALL + - - ENTER - 切換 + + Target Branch + - - The new branch will be pulled the next time the updater runs. - 新的分支將會在下次檢查更新時切換過去。 + SELECT + - - Enter branch name - 輸入分支名稱 + + Select a branch + - + UNINSTALL 卸載 @@ -1059,13 +1038,8 @@ location set 您確定您要卸載嗎? - - failed to fetch update - 下載更新失敗 - - - - + + CHECK 檢查 @@ -1083,7 +1057,7 @@ location set 警告:這將授權給 GitHub 帳號中所有公鑰 SSH 訪問權限。切勿輸入非您自己的 GitHub 用戶名。comma 員工「永遠不會」要求您添加他們的 GitHub 用戶名。 - + ADD 新增 @@ -1153,7 +1127,7 @@ location set TogglesPanel - + Enable openpilot 啟用 openpilot @@ -1290,12 +1264,11 @@ location set WifiUI - Scanning for networks... 掃描無線網路中... - + CONNECTING... 連線中... diff --git a/selfdrive/updated.py b/selfdrive/updated.py index 65f8425201..79b759a906 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -23,6 +23,7 @@ # disable this service. import os +import re import datetime import subprocess import psutil @@ -31,8 +32,9 @@ import signal import fcntl import time import threading +from collections import defaultdict from pathlib import Path -from typing import List, Tuple, Optional +from typing import List, Union, Optional from markdown_it import MarkdownIt from common.basedir import BASEDIR @@ -54,20 +56,27 @@ DAYS_NO_CONNECTIVITY_MAX = 14 # do not allow to engage after this many days DAYS_NO_CONNECTIVITY_PROMPT = 10 # send an offroad prompt after this many days class WaitTimeHelper: - def __init__(self, proc): - self.proc = proc + def __init__(self): self.ready_event = threading.Event() + self.only_check_for_update = False signal.signal(signal.SIGHUP, self.update_now) + signal.signal(signal.SIGUSR1, self.check_now) def update_now(self, signum: int, frame) -> None: - cloudlog.info("caught SIGHUP, running update check immediately") + cloudlog.info("caught SIGHUP, attempting to downloading update") + self.only_check_for_update = False + self.ready_event.set() + + def check_now(self, signum: int, frame) -> None: + cloudlog.info("caught SIGUSR1, checking for updates") + self.only_check_for_update = True self.ready_event.set() def sleep(self, t: float) -> None: self.ready_event.wait(timeout=t) -def run(cmd: List[str], cwd: Optional[str] = None): +def run(cmd: List[str], cwd: Optional[str] = None) -> str: return subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8') @@ -80,59 +89,19 @@ def set_consistent_flag(consistent: bool) -> None: consistent_file.unlink(missing_ok=True) os.sync() - -def set_params(new_version: bool, failed_count: int, exception: Optional[str]) -> None: - params = Params() - - params.put("UpdateFailedCount", str(failed_count)) - - last_update = datetime.datetime.utcnow() - if failed_count == 0: - t = last_update.isoformat() - params.put("LastUpdateTime", t.encode('utf8')) - else: - try: - t = params.get("LastUpdateTime", encoding='utf8') - last_update = datetime.datetime.fromisoformat(t) - except (TypeError, ValueError): - pass - - if exception is None: - params.remove("LastUpdateException") - else: - params.put("LastUpdateException", exception) - - # Write out release notes for new versions - if new_version: +def parse_release_notes(basedir: str) -> bytes: + try: + with open(os.path.join(basedir, "RELEASES.md"), "rb") as f: + r = f.read().split(b'\n\n', 1)[0] # Slice latest release notes try: - with open(os.path.join(FINALIZED, "RELEASES.md"), "rb") as f: - r = f.read().split(b'\n\n', 1)[0] # Slice latest release notes - try: - params.put("ReleaseNotes", MarkdownIt().render(r.decode("utf-8"))) - except Exception: - params.put("ReleaseNotes", r + b"\n") + return bytes(MarkdownIt().render(r.decode("utf-8")), encoding="utf-8") except Exception: - params.put("ReleaseNotes", "") - params.put_bool("UpdateAvailable", True) - - # Handle user prompt - for alert in ("Offroad_UpdateFailed", "Offroad_ConnectivityNeeded", "Offroad_ConnectivityNeededPrompt"): - set_offroad_alert(alert, False) - - now = datetime.datetime.utcnow() - dt = now - last_update - if failed_count > 15 and exception is not None: - if is_tested_branch(): - extra_text = "Ensure the software is correctly installed" - else: - extra_text = exception - set_offroad_alert("Offroad_UpdateFailed", True, extra_text=extra_text) - elif dt.days > DAYS_NO_CONNECTIVITY_MAX and failed_count > 1: - set_offroad_alert("Offroad_ConnectivityNeeded", True) - elif dt.days > DAYS_NO_CONNECTIVITY_PROMPT: - remaining = max(DAYS_NO_CONNECTIVITY_MAX - dt.days, 1) - set_offroad_alert("Offroad_ConnectivityNeededPrompt", True, extra_text=f"{remaining} day{'' if remaining == 1 else 's'}.") - + return r + b"\n" + except FileNotFoundError: + pass + except Exception: + cloudlog.exception("failed to parse release notes") + return b"" def setup_git_options(cwd: str) -> None: # We sync FS object atimes (which NEOS doesn't use) and mtimes, but ctimes @@ -267,65 +236,161 @@ def handle_agnos_update() -> None: set_offroad_alert("Offroad_NeosUpdate", False) -def check_git_fetch_result(fetch_txt: str) -> bool: - err_msg = "Failed to add the host to the list of known hosts (/data/data/com.termux/files/home/.ssh/known_hosts).\n" - return len(fetch_txt) > 0 and (fetch_txt != err_msg) +class Updater: + def __init__(self): + self.params = Params() + self.branches = defaultdict(lambda: None) + + @property + def target_branch(self) -> str: + b: Union[str, None] = self.params.get("UpdaterTargetBranch", encoding='utf-8') + if b is None: + b = self.get_branch(BASEDIR) + self.params.put("UpdaterTargetBranch", b) + return b + + @property + def update_ready(self) -> bool: + consistent_file = Path(os.path.join(FINALIZED, ".overlay_consistent")) + if consistent_file.is_file(): + hash_mismatch = self.get_commit_hash(BASEDIR) != self.branches[self.target_branch] + branch_mismatch = self.get_branch(BASEDIR) != self.target_branch + on_target_branch = self.get_branch(FINALIZED) == self.target_branch + return ((hash_mismatch or branch_mismatch) and on_target_branch) + return False + + @property + def update_available(self) -> bool: + if os.path.isdir(OVERLAY_MERGED): + hash_mismatch = self.get_commit_hash(OVERLAY_MERGED) != self.branches[self.target_branch] + branch_mismatch = self.get_branch(OVERLAY_MERGED) != self.target_branch + return hash_mismatch or branch_mismatch + return False + + def get_branch(self, path: str) -> str: + return run(["git", "rev-parse", "--abbrev-ref", "HEAD"], path).rstrip() + + def get_commit_hash(self, path: str = OVERLAY_MERGED) -> str: + return run(["git", "rev-parse", "HEAD"], path).rstrip() + + def set_params(self, failed_count: int, exception: Optional[str]) -> None: + self.params.put("UpdateFailedCount", str(failed_count)) + + self.params.put_bool("UpdaterFetchAvailable", self.update_available) + self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys())) + + last_update = datetime.datetime.utcnow() + if failed_count == 0: + t = last_update.isoformat() + self.params.put("LastUpdateTime", t.encode('utf8')) + else: + try: + t = self.params.get("LastUpdateTime", encoding='utf8') + last_update = datetime.datetime.fromisoformat(t) + except (TypeError, ValueError): + pass -def check_for_update() -> Tuple[bool, bool]: - setup_git_options(OVERLAY_MERGED) - try: - git_fetch_output = run(["git", "fetch", "--dry-run"], OVERLAY_MERGED) - return True, check_git_fetch_result(git_fetch_output) - except subprocess.CalledProcessError: - return False, False - + if exception is None: + self.params.remove("LastUpdateException") + else: + self.params.put("LastUpdateException", exception) -def fetch_update() -> bool: - cloudlog.info("attempting git fetch inside staging overlay") + # Write out current and new version info + def get_description(basedir: str) -> str: + version = "" + branch = "" + commit = "" + try: + branch = self.get_branch(basedir) + commit = self.get_commit_hash(basedir) + with open(os.path.join(basedir, "common", "version.h")) as f: + version = f.read().split('"')[1] + except Exception: + pass + return f"{version} / {branch} / {commit[:7]}" + self.params.put("UpdaterCurrentDescription", get_description(BASEDIR)) + self.params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR)) + self.params.put("UpdaterNewDescription", get_description(FINALIZED)) + self.params.put("UpdaterNewReleaseNotes", parse_release_notes(FINALIZED)) + self.params.put_bool("UpdateAvailable", self.update_ready) + + # Handle user prompt + for alert in ("Offroad_UpdateFailed", "Offroad_ConnectivityNeeded", "Offroad_ConnectivityNeededPrompt"): + set_offroad_alert(alert, False) + + now = datetime.datetime.utcnow() + dt = now - last_update + if failed_count > 15 and exception is not None: + if is_tested_branch(): + extra_text = "Ensure the software is correctly installed. Uninstall and re-install if this error persists." + else: + extra_text = exception + set_offroad_alert("Offroad_UpdateFailed", True, extra_text=extra_text) + elif dt.days > DAYS_NO_CONNECTIVITY_MAX and failed_count > 1: + set_offroad_alert("Offroad_ConnectivityNeeded", True) + elif dt.days > DAYS_NO_CONNECTIVITY_PROMPT: + remaining = max(DAYS_NO_CONNECTIVITY_MAX - dt.days, 1) + set_offroad_alert("Offroad_ConnectivityNeededPrompt", True, extra_text=f"{remaining} day{'' if remaining == 1 else 's'}.") + + def check_for_update(self) -> None: + cloudlog.info("checking for updates") + + excluded_branches = ('release2', 'release2-staging', 'dashcam', 'dashcam-staging') + + setup_git_options(OVERLAY_MERGED) + output = run(["git", "ls-remote", "--heads"], OVERLAY_MERGED) + + self.branches = defaultdict(lambda: None) + for line in output.split('\n'): + ls_remotes_re = r'(?P\b[0-9a-f]{5,40}\b)(\s+)(refs\/heads\/)(?P.*$)' + x = re.fullmatch(ls_remotes_re, line.strip()) + if x is not None and x.group('branch_name') not in excluded_branches: + self.branches[x.group('branch_name')] = x.group('commit_sha') + + cur_branch = self.get_branch(OVERLAY_MERGED) + cur_commit = self.get_commit_hash(OVERLAY_MERGED) + new_branch = self.target_branch + new_commit = self.branches[new_branch] + if (cur_branch, cur_commit) != (new_branch, new_commit): + cloudlog.info(f"update available, {cur_branch} ({cur_commit[:7]}) -> {new_branch} ({new_commit[:7]})") + else: + cloudlog.info(f"up to date on {cur_branch} ({cur_commit[:7]})") - setup_git_options(OVERLAY_MERGED) + def fetch_update(self) -> None: + cloudlog.info("attempting git fetch inside staging overlay") - git_fetch_output = run(["git", "fetch"], OVERLAY_MERGED) - cloudlog.info("git fetch success: %s", git_fetch_output) + self.params.put("UpdaterState", "downloading...") - cur_hash = run(["git", "rev-parse", "HEAD"], OVERLAY_MERGED).rstrip() - upstream_hash = run(["git", "rev-parse", "@{u}"], OVERLAY_MERGED).rstrip() - new_version: bool = cur_hash != upstream_hash - git_fetch_result = check_git_fetch_result(git_fetch_output) + # TODO: cleanly interrupt this and invalidate old update + set_consistent_flag(False) + self.params.put_bool("UpdateAvailable", False) - new_branch = Params().get("SwitchToBranch", encoding='utf8') - if new_branch is not None: - new_version = True + setup_git_options(OVERLAY_MERGED) - cloudlog.info(f"comparing {cur_hash} to {upstream_hash}") - if new_version or git_fetch_result: - cloudlog.info("Running update") + branch = self.target_branch + git_fetch_output = run(["git", "fetch", "origin", branch], OVERLAY_MERGED) + cloudlog.info("git fetch success: %s", git_fetch_output) - if new_version: - cloudlog.info("git reset in progress") - cmds = [ - ["git", "reset", "--hard", "@{u}"], - ["git", "clean", "-xdf"], - ["git", "submodule", "init"], - ["git", "submodule", "update"], - ] - if new_branch is not None: - cloudlog.info(f"switching to branch {repr(new_branch)}") - cmds.insert(0, ["git", "checkout", "-f", new_branch]) - r = [run(cmd, OVERLAY_MERGED) for cmd in cmds] - cloudlog.info("git reset success: %s", '\n'.join(r)) + cloudlog.info("git reset in progress") + cmds = [ + ["git", "checkout", "--force", "--no-recurse-submodules", branch], + ["git", "reset", "--hard", f"origin/{branch}"], + ["git", "clean", "-xdf"], + ["git", "submodule", "init"], + ["git", "submodule", "update"], + ] + r = [run(cmd, OVERLAY_MERGED) for cmd in cmds] + cloudlog.info("git reset success: %s", '\n'.join(r)) - if AGNOS: - handle_agnos_update() + # TODO: show agnos download progress + if AGNOS: + handle_agnos_update() # Create the finalized, ready-to-swap update + self.params.put("UpdaterState", "finalizing update...") finalize_update() - cloudlog.info("openpilot update successful!") - else: - cloudlog.info("nothing new from git at this time") - - return new_version + cloudlog.info("finalize success!") def main() -> None: @@ -357,8 +422,12 @@ def main() -> None: overlay_init = Path(os.path.join(BASEDIR, ".overlay_init")) overlay_init.unlink(missing_ok=True) + updater = Updater() update_failed_count = 0 # TODO: Load from param? - wait_helper = WaitTimeHelper(proc) + + # no fetch on the first time + wait_helper = WaitTimeHelper() + wait_helper.only_check_for_update = True # Run the update loop while True: @@ -366,21 +435,24 @@ def main() -> None: # Attempt an update exception = None - new_version = False - update_failed_count += 1 try: + # TODO: reuse overlay from previous updated instance if it looks clean init_overlay() - # TODO: still needed? skip this and just fetch? - # Lightweight internt check - internet_ok, update_available = check_for_update() - if internet_ok and not update_available: - update_failed_count = 0 + # ensure we have some params written soon after startup + updater.set_params(update_failed_count, exception) + update_failed_count += 1 + + # check for update + params.put("UpdaterState", "checking...") + updater.check_for_update() - # Fetch update - if internet_ok: - new_version = fetch_update() - update_failed_count = 0 + # download update + if wait_helper.only_check_for_update: + cloudlog.info("skipping fetch this cycle") + else: + updater.fetch_update() + update_failed_count = 0 except subprocess.CalledProcessError as e: cloudlog.event( "update process failed", @@ -396,12 +468,14 @@ def main() -> None: overlay_init.unlink(missing_ok=True) try: - set_params(new_version, update_failed_count, exception) + params.put("UpdaterState", "idle") + updater.set_params(update_failed_count, exception) except Exception: cloudlog.exception("uncaught updated exception while setting params, shouldn't happen") # infrequent attempts if we successfully updated recently - wait_helper.sleep(5*60 if update_failed_count > 0 else 90*60) + wait_helper.only_check_for_update = False + wait_helper.sleep(5*60 if update_failed_count > 0 else 1.5*60*60) if __name__ == "__main__":