good updater experience (#25724)

* good updater experience

* set params on startup

* no fetch on first loop

* little type hinting

* little more

* update translations

* always set params with valid overlay

* wrap check

* use the param

* more wrapping

* vanish

* cleanup

* remove that
old-commit-hash: c4e63d14ab
taco
Adeeb Shihadeh 3 years ago committed by GitHub
parent 7cd138328e
commit 46cfb5c45b
  1. 10
      common/params.cc
  2. 2
      selfdrive/ui/.gitignore
  3. 3
      selfdrive/ui/SConscript
  4. 81
      selfdrive/ui/qt/offroad/settings.cc
  5. 13
      selfdrive/ui/qt/offroad/settings.h
  6. 156
      selfdrive/ui/qt/offroad/software_settings.cc
  7. 8
      selfdrive/ui/qt/widgets/controls.cc
  8. 14
      selfdrive/ui/qt/widgets/controls.h
  9. 2
      selfdrive/ui/qt/widgets/offroad_alerts.cc
  10. 8
      selfdrive/ui/qt/widgets/ssh_keys.cc
  11. 2
      selfdrive/ui/qt/widgets/ssh_keys.h
  12. 2
      selfdrive/ui/tests/cycle_offroad_alerts.py
  13. 91
      selfdrive/ui/translations/main_ja.ts
  14. 91
      selfdrive/ui/translations/main_ko.ts
  15. 91
      selfdrive/ui/translations/main_pt-BR.ts
  16. 91
      selfdrive/ui/translations/main_zh-CHS.ts
  17. 91
      selfdrive/ui/translations/main_zh-CHT.ts
  18. 306
      selfdrive/updated.py

@ -154,18 +154,24 @@ std::unordered_map<std::string, uint32_t> keys = {
{"PrimeType", PERSISTENT}, {"PrimeType", PERSISTENT},
{"RecordFront", PERSISTENT}, {"RecordFront", PERSISTENT},
{"RecordFrontLock", PERSISTENT}, // for the internal fleet {"RecordFrontLock", PERSISTENT}, // for the internal fleet
{"ReleaseNotes", PERSISTENT},
{"ReplayControlsState", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"ReplayControlsState", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON},
{"ShouldDoUpdate", CLEAR_ON_MANAGER_START}, {"ShouldDoUpdate", CLEAR_ON_MANAGER_START},
{"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF},
{"SshEnabled", PERSISTENT}, {"SshEnabled", PERSISTENT},
{"SubscriberInfo", PERSISTENT}, {"SubscriberInfo", PERSISTENT},
{"SwitchToBranch", CLEAR_ON_MANAGER_START},
{"TermsVersion", PERSISTENT}, {"TermsVersion", PERSISTENT},
{"Timezone", PERSISTENT}, {"Timezone", PERSISTENT},
{"TrainingVersion", PERSISTENT}, {"TrainingVersion", PERSISTENT},
{"UpdateAvailable", CLEAR_ON_MANAGER_START}, {"UpdateAvailable", CLEAR_ON_MANAGER_START},
{"UpdateFailedCount", 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}, {"Version", PERSISTENT},
{"VisionRadarToggle", PERSISTENT}, {"VisionRadarToggle", PERSISTENT},
{"WideCameraOnly", PERSISTENT}, {"WideCameraOnly", PERSISTENT},

@ -1,6 +1,8 @@
moc_* moc_*
*.moc *.moc
translations/main_test_en.*
_mui _mui
watch3 watch3
installer/installers/* installer/installers/*

@ -56,7 +56,8 @@ qt_env.Program("qt/spinner", ["qt/spinner.cc"], LIBS=qt_libs)
# build main UI # build main UI
qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc", qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc",
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.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) qt_env.Program("_ui", qt_src + [asset_obj], LIBS=qt_libs)
if GetOption('test'): if GetOption('test'):
qt_src.remove("main.cc") # replaced by test_runner qt_src.remove("main.cc") # replaced by test_runner

@ -159,7 +159,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
addItem(dcamBtn); addItem(dcamBtn);
auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); 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, [&]() { connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), this)) { if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), this)) {
params.remove("CalibrationParams"); 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) { void SettingsWindow::showEvent(QShowEvent *event) {
panel_widget->setCurrentIndex(0); panel_widget->setCurrentIndex(0);
nav_btns->buttons()[0]->setChecked(true); nav_btns->buttons()[0]->setChecked(true);

@ -71,14 +71,15 @@ public:
private: private:
void showEvent(QShowEvent *event) override; void showEvent(QShowEvent *event) override;
void updateLabels(); void updateLabels();
void checkForUpdates();
LabelControl *gitBranchLbl; bool is_onroad = false;
LabelControl *gitCommitLbl;
LabelControl *osVersionLbl; QLabel *onroadLbl;
LabelControl *versionLbl; LabelControl *versionLbl;
LabelControl *lastUpdateLbl; ButtonControl *installBtn;
ButtonControl *updateBtn; ButtonControl *downloadBtn;
ButtonControl *branchSwitcherBtn; ButtonControl *targetBranchBtn;
Params params; Params params;
QFileSystemWatcher *fs_watch; QFileSystemWatcher *fs_watch;

@ -0,0 +1,156 @@
#include "selfdrive/ui/qt/offroad/settings.h"
#include <cassert>
#include <cmath>
#include <string>
#include <QDebug>
#include <QLabel>
#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();
}

@ -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"); title_label->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left");
hlayout->addWidget(title_label); 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); main_layout->addLayout(hlayout);
// description // description
@ -54,7 +60,7 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons
connect(title_label, &QPushButton::clicked, [=]() { connect(title_label, &QPushButton::clicked, [=]() {
if (!description->isVisible()) { if (!description->isVisible()) {
emit showDescription(); emit showDescriptionEvent();
} }
if (!description->text().isEmpty()) { if (!description->text().isEmpty()) {

@ -45,8 +45,17 @@ public:
title_label->setText(title); title_label->setText(title);
} }
void setValue(const QString &val) {
value->setText(val);
}
public slots:
void showDescription() {
description->setVisible(true);
};
signals: signals:
void showDescription(); void showDescriptionEvent();
protected: protected:
AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr);
@ -54,6 +63,9 @@ protected:
QHBoxLayout *hlayout; QHBoxLayout *hlayout;
QPushButton *title_label; QPushButton *title_label;
private:
QLabel *value;
QLabel *description = nullptr; QLabel *description = nullptr;
}; };

@ -112,7 +112,7 @@ UpdateAlert::UpdateAlert(QWidget *parent) : AbstractAlert(true, parent) {
bool UpdateAlert::refresh() { bool UpdateAlert::refresh() {
bool updateAvailable = params.getBool("UpdateAvailable"); bool updateAvailable = params.getBool("UpdateAvailable");
if (updateAvailable) { if (updateAvailable) {
releaseNotes->setText(params.get("ReleaseNotes").c_str()); releaseNotes->setText(params.get("UpdaterNewReleaseNotes").c_str());
} }
return updateAvailable; return updateAvailable;
} }

@ -5,10 +5,6 @@
#include "selfdrive/ui/qt/widgets/input.h" #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.")) { 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, [=]() { QObject::connect(this, &ButtonControl::clicked, [=]() {
if (text() == tr("ADD")) { if (text() == tr("ADD")) {
QString username = InputDialog::getText(tr("Enter your GitHub username"), this); 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() { void SshControl::refresh() {
QString param = QString::fromStdString(params.get("GithubSshKeys")); QString param = QString::fromStdString(params.get("GithubSshKeys"));
if (param.length()) { if (param.length()) {
username_label.setText(QString::fromStdString(params.get("GithubUsername"))); setValue(QString::fromStdString(params.get("GithubUsername")));
setText(tr("REMOVE")); setText(tr("REMOVE"));
} else { } else {
username_label.setText(""); setValue("");
setText(tr("ADD")); setText(tr("ADD"));
} }
setEnabled(true); setEnabled(true);

@ -27,8 +27,6 @@ public:
private: private:
Params params; Params params;
QLabel username_label;
void refresh(); void refresh();
void getUserKeys(const QString &username); void getUserKeys(const QString &username);
}; };

@ -20,7 +20,7 @@ if __name__ == "__main__":
params.put_bool("UpdateAvailable", True) params.put_bool("UpdateAvailable", True)
r = open(os.path.join(BASEDIR, "RELEASES.md")).read() r = open(os.path.join(BASEDIR, "RELEASES.md")).read()
r = r[:r.find('\n\n')] # Slice latest release notes r = r[:r.find('\n\n')] # Slice latest release notes
params.put("ReleaseNotes", r + "\n") params.put("UpdaterNewReleaseNotes", r + "\n")
time.sleep(t) time.sleep(t)
params.put_bool("UpdateAvailable", False) params.put_bool("UpdateAvailable", False)

@ -193,7 +193,7 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+4"/> <location line="+3"/>
<source>Select a language</source> <source>Select a language</source>
<translation></translation> <translation></translation>
</message> </message>
@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai</source>
https://connect.comma.ai</translation> https://connect.comma.ai</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+58"/>
<source>No home <source>No home
location set</source> location set</source>
<translation> <translation>
@ -432,7 +432,7 @@ location set</source>
</translation> </translation>
</message> </message>
<message> <message>
<location line="+113"/> <location line="+120"/>
<source>no recent destinations</source> <source>no recent destinations</source>
<translation></translation> <translation></translation>
</message> </message>
@ -718,7 +718,7 @@ location set</source>
<context> <context>
<name>SettingsWindow</name> <name>SettingsWindow</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="+101"/> <location filename="../qt/offroad/settings.cc" line="+22"/>
<source>×</source> <source>×</source>
<translation>×</translation> <translation>×</translation>
</message> </message>
@ -983,68 +983,47 @@ location set</source>
<context> <context>
<name>SoftwarePanel</name> <name>SoftwarePanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-130"/> <location filename="../qt/offroad/software_settings.cc" line="+24"/>
<source>Git Branch</source> <source>Updates are only downloaded while the car is off.</source>
<translation>Git </translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+5"/>
<source>Git Commit</source> <source>Current Version</source>
<translation>Git </translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>OS Version</source>
<translation>OS </translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+4"/>
<source>Version</source> <source>Download</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+12"/>
<source>Last Update Check</source> <source>Install Update</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<source>The last time openpilot successfully checked for an update. The updater only runs while the car is off.</source> <source>INSTALL</source>
<translation>openpilotが最後にアップデートの確認に成功してからの時間です</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Check for Update</source>
<translation></translation>
</message>
<message>
<location line="+5"/>
<source>CHECKING</source>
<translation></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+8"/>
<source>Switch Branch</source> <source>Target Branch</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<source>ENTER</source> <source>SELECT</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+13"/>
<location line="+2"/> <source>Select a branch</source>
<source>The new branch will be pulled the next time the updater runs.</source> <translation type="unfinished"></translation>
<translation>updater </translation>
</message>
<message>
<location line="+0"/>
<source>Enter branch name</source>
<translation></translation>
</message> </message>
<message> <message>
<location line="+11"/> <location line="+12"/>
<source>UNINSTALL</source> <source>UNINSTALL</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1059,13 +1038,8 @@ location set</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+17"/> <location line="-47"/>
<source>failed to fetch update</source> <location line="+3"/>
<translation></translation>
</message>
<message>
<location line="+1"/>
<location line="+21"/>
<source>CHECK</source> <source>CHECK</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1083,7 +1057,7 @@ location set</source>
<translation>警告: これはGitHub SSH GitHub GitHub </translation> <translation>警告: これはGitHub SSH GitHub GitHub </translation>
</message> </message>
<message> <message>
<location line="+6"/> <location line="+2"/>
<location line="+24"/> <location line="+24"/>
<source>ADD</source> <source>ADD</source>
<translation></translation> <translation></translation>
@ -1153,7 +1127,7 @@ location set</source>
<context> <context>
<name>TogglesPanel</name> <name>TogglesPanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-324"/> <location filename="../qt/offroad/settings.cc" line="-303"/>
<source>Enable openpilot</source> <source>Enable openpilot</source>
<translation>openpilot </translation> <translation>openpilot </translation>
</message> </message>
@ -1290,12 +1264,11 @@ location set</source>
<name>WifiUI</name> <name>WifiUI</name>
<message> <message>
<location filename="../qt/offroad/networking.cc" line="+113"/> <location filename="../qt/offroad/networking.cc" line="+113"/>
<location line="+53"/>
<source>Scanning for networks...</source> <source>Scanning for networks...</source>
<translation>...</translation> <translation>...</translation>
</message> </message>
<message> <message>
<location line="+26"/> <location line="+80"/>
<source>CONNECTING...</source> <source>CONNECTING...</source>
<translation>...</translation> <translation>...</translation>
</message> </message>

@ -193,7 +193,7 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+4"/> <location line="+3"/>
<source>Select a language</source> <source>Select a language</source>
<translation> </translation> <translation> </translation>
</message> </message>
@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai</source>
https://connect.comma.ai</translation> https://connect.comma.ai</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+58"/>
<source>No home <source>No home
location set</source> location set</source>
<translation> <translation>
@ -432,7 +432,7 @@ location set</source>
</translation> </translation>
</message> </message>
<message> <message>
<location line="+113"/> <location line="+120"/>
<source>no recent destinations</source> <source>no recent destinations</source>
<translation> </translation> <translation> </translation>
</message> </message>
@ -718,7 +718,7 @@ location set</source>
<context> <context>
<name>SettingsWindow</name> <name>SettingsWindow</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="+101"/> <location filename="../qt/offroad/settings.cc" line="+22"/>
<source>×</source> <source>×</source>
<translation>×</translation> <translation>×</translation>
</message> </message>
@ -983,68 +983,47 @@ location set</source>
<context> <context>
<name>SoftwarePanel</name> <name>SoftwarePanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-130"/> <location filename="../qt/offroad/software_settings.cc" line="+24"/>
<source>Git Branch</source> <source>Updates are only downloaded while the car is off.</source>
<translation>Git </translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+5"/>
<source>Git Commit</source> <source>Current Version</source>
<translation>Git </translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>OS Version</source>
<translation>OS </translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+4"/>
<source>Version</source> <source>Download</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+12"/>
<source>Last Update Check</source> <source>Install Update</source>
<translation> </translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<source>The last time openpilot successfully checked for an update. The updater only runs while the car is off.</source> <source>INSTALL</source>
<translation> openpilot이 . .</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Check for Update</source>
<translation> </translation>
</message>
<message>
<location line="+5"/>
<source>CHECKING</source>
<translation></translation>
</message> </message>
<message> <message>
<location line="+7"/> <location line="+8"/>
<source>Switch Branch</source> <source>Target Branch</source>
<translation> </translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<source>ENTER</source> <source>SELECT</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+13"/>
<location line="+2"/> <source>Select a branch</source>
<source>The new branch will be pulled the next time the updater runs.</source> <translation type="unfinished"></translation>
<translation> .</translation>
</message>
<message>
<location line="+0"/>
<source>Enter branch name</source>
<translation> </translation>
</message> </message>
<message> <message>
<location line="+11"/> <location line="+12"/>
<source>UNINSTALL</source> <source>UNINSTALL</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1059,13 +1038,8 @@ location set</source>
<translation>?</translation> <translation>?</translation>
</message> </message>
<message> <message>
<location line="+17"/> <location line="-47"/>
<source>failed to fetch update</source> <location line="+3"/>
<translation> </translation>
</message>
<message>
<location line="+1"/>
<location line="+21"/>
<source>CHECK</source> <source>CHECK</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1083,7 +1057,7 @@ location set</source>
<translation>경고: 허용으로 GitHub SSH . GitHub ID . comma에서는 GitHub ID를 .</translation> <translation>경고: 허용으로 GitHub SSH . GitHub ID . comma에서는 GitHub ID를 .</translation>
</message> </message>
<message> <message>
<location line="+6"/> <location line="+2"/>
<location line="+24"/> <location line="+24"/>
<source>ADD</source> <source>ADD</source>
<translation></translation> <translation></translation>
@ -1153,7 +1127,7 @@ location set</source>
<context> <context>
<name>TogglesPanel</name> <name>TogglesPanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-324"/> <location filename="../qt/offroad/settings.cc" line="-303"/>
<source>Enable openpilot</source> <source>Enable openpilot</source>
<translation>openpilot </translation> <translation>openpilot </translation>
</message> </message>
@ -1290,12 +1264,11 @@ location set</source>
<name>WifiUI</name> <name>WifiUI</name>
<message> <message>
<location filename="../qt/offroad/networking.cc" line="+113"/> <location filename="../qt/offroad/networking.cc" line="+113"/>
<location line="+53"/>
<source>Scanning for networks...</source> <source>Scanning for networks...</source>
<translation> ...</translation> <translation> ...</translation>
</message> </message>
<message> <message>
<location line="+26"/> <location line="+80"/>
<source>CONNECTING...</source> <source>CONNECTING...</source>
<translation>...</translation> <translation>...</translation>
</message> </message>

@ -193,7 +193,7 @@
<translation>ALTERAR</translation> <translation>ALTERAR</translation>
</message> </message>
<message> <message>
<location line="+4"/> <location line="+3"/>
<source>Select a language</source> <source>Select a language</source>
<translation>Selecione o Idioma</translation> <translation>Selecione o Idioma</translation>
</message> </message>
@ -419,7 +419,7 @@ prime subscription. Sign up now: https://connect.comma.ai</source>
uma assinatura prime Inscreva-se agora: https://connect.comma.ai</translation> uma assinatura prime Inscreva-se agora: https://connect.comma.ai</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+58"/>
<source>No home <source>No home
location set</source> location set</source>
<translation>Sem local <translation>Sem local
@ -433,7 +433,7 @@ location set</source>
trabalho definido</translation> trabalho definido</translation>
</message> </message>
<message> <message>
<location line="+113"/> <location line="+120"/>
<source>no recent destinations</source> <source>no recent destinations</source>
<translation>sem destinos recentes</translation> <translation>sem destinos recentes</translation>
</message> </message>
@ -722,7 +722,7 @@ trabalho definido</translation>
<context> <context>
<name>SettingsWindow</name> <name>SettingsWindow</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="+101"/> <location filename="../qt/offroad/settings.cc" line="+22"/>
<source>×</source> <source>×</source>
<translation>×</translation> <translation>×</translation>
</message> </message>
@ -987,68 +987,47 @@ trabalho definido</translation>
<context> <context>
<name>SoftwarePanel</name> <name>SoftwarePanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-130"/> <location filename="../qt/offroad/software_settings.cc" line="+24"/>
<source>Git Branch</source> <source>Updates are only downloaded while the car is off.</source>
<translation>Git Branch</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Git Commit</source>
<translation>Último Commit</translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+5"/>
<source>OS Version</source> <source>Current Version</source>
<translation>Versão do Sistema</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+4"/>
<source>Version</source> <source>Download</source>
<translation>Versão</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+12"/>
<source>Last Update Check</source> <source>Install Update</source>
<translation>Verificação da última atualização</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<source>The last time openpilot successfully checked for an update. The updater only runs while the car is off.</source> <source>INSTALL</source>
<translation>A última vez que o openpilot verificou com sucesso uma atualização. O atualizador funciona com o carro desligado.</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Check for Update</source>
<translation>Verifique atualizações</translation>
</message>
<message>
<location line="+5"/>
<source>CHECKING</source>
<translation>VERIFICANDO</translation>
</message>
<message>
<location line="+7"/>
<source>Switch Branch</source>
<translation>Alterar Branch</translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+8"/>
<source>ENTER</source> <source>Target Branch</source>
<translation>INSERIR</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<location line="+2"/> <source>SELECT</source>
<source>The new branch will be pulled the next time the updater runs.</source> <translation type="unfinished"></translation>
<translation>A nova branch será aplicada ao verificar atualizações.</translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+13"/>
<source>Enter branch name</source> <source>Select a branch</source>
<translation>Inserir o nome da branch</translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+11"/> <location line="+12"/>
<source>UNINSTALL</source> <source>UNINSTALL</source>
<translation>DESINSTALAR</translation> <translation>DESINSTALAR</translation>
</message> </message>
@ -1063,13 +1042,8 @@ trabalho definido</translation>
<translation>Tem certeza que quer desinstalar?</translation> <translation>Tem certeza que quer desinstalar?</translation>
</message> </message>
<message> <message>
<location line="+17"/> <location line="-47"/>
<source>failed to fetch update</source> <location line="+3"/>
<translation>falha ao buscar atualização</translation>
</message>
<message>
<location line="+1"/>
<location line="+21"/>
<source>CHECK</source> <source>CHECK</source>
<translation>VERIFICAR</translation> <translation>VERIFICAR</translation>
</message> </message>
@ -1087,7 +1061,7 @@ trabalho definido</translation>
<translation>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.</translation> <translation>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.</translation>
</message> </message>
<message> <message>
<location line="+6"/> <location line="+2"/>
<location line="+24"/> <location line="+24"/>
<source>ADD</source> <source>ADD</source>
<translation>ADICIONAR</translation> <translation>ADICIONAR</translation>
@ -1157,7 +1131,7 @@ trabalho definido</translation>
<context> <context>
<name>TogglesPanel</name> <name>TogglesPanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-324"/> <location filename="../qt/offroad/settings.cc" line="-303"/>
<source>Enable openpilot</source> <source>Enable openpilot</source>
<translation>Ativar openpilot</translation> <translation>Ativar openpilot</translation>
</message> </message>
@ -1294,12 +1268,11 @@ trabalho definido</translation>
<name>WifiUI</name> <name>WifiUI</name>
<message> <message>
<location filename="../qt/offroad/networking.cc" line="+113"/> <location filename="../qt/offroad/networking.cc" line="+113"/>
<location line="+53"/>
<source>Scanning for networks...</source> <source>Scanning for networks...</source>
<translation>Procurando redes...</translation> <translation>Procurando redes...</translation>
</message> </message>
<message> <message>
<location line="+26"/> <location line="+80"/>
<source>CONNECTING...</source> <source>CONNECTING...</source>
<translation>CONECTANDO...</translation> <translation>CONECTANDO...</translation>
</message> </message>

@ -193,7 +193,7 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+4"/> <location line="+3"/>
<source>Select a language</source> <source>Select a language</source>
<translation></translation> <translation></translation>
</message> </message>
@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai</source>
https://connect.comma.ai</translation> https://connect.comma.ai</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+58"/>
<source>No home <source>No home
location set</source> location set</source>
<translation></translation> <translation></translation>
@ -430,7 +430,7 @@ location set</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+113"/> <location line="+120"/>
<source>no recent destinations</source> <source>no recent destinations</source>
<translation></translation> <translation></translation>
</message> </message>
@ -716,7 +716,7 @@ location set</source>
<context> <context>
<name>SettingsWindow</name> <name>SettingsWindow</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="+101"/> <location filename="../qt/offroad/settings.cc" line="+22"/>
<source>×</source> <source>×</source>
<translation>×</translation> <translation>×</translation>
</message> </message>
@ -981,68 +981,47 @@ location set</source>
<context> <context>
<name>SoftwarePanel</name> <name>SoftwarePanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-130"/> <location filename="../qt/offroad/software_settings.cc" line="+24"/>
<source>Git Branch</source> <source>Updates are only downloaded while the car is off.</source>
<translation>Git Branch</translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Git Commit</source>
<translation>Git Commit</translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+5"/>
<source>OS Version</source> <source>Current Version</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+4"/>
<source>Version</source> <source>Download</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+12"/>
<source>Last Update Check</source> <source>Install Update</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<source>The last time openpilot successfully checked for an update. The updater only runs while the car is off.</source> <source>INSTALL</source>
<translation></translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Check for Update</source>
<translation></translation>
</message>
<message>
<location line="+5"/>
<source>CHECKING</source>
<translation></translation>
</message>
<message>
<location line="+7"/>
<source>Switch Branch</source>
<translation></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+8"/>
<source>ENTER</source> <source>Target Branch</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<location line="+2"/> <source>SELECT</source>
<source>The new branch will be pulled the next time the updater runs.</source> <translation type="unfinished"></translation>
<translation></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+13"/>
<source>Enter branch name</source> <source>Select a branch</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+11"/> <location line="+12"/>
<source>UNINSTALL</source> <source>UNINSTALL</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1057,13 +1036,8 @@ location set</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+17"/> <location line="-47"/>
<source>failed to fetch update</source> <location line="+3"/>
<translation></translation>
</message>
<message>
<location line="+1"/>
<location line="+21"/>
<source>CHECK</source> <source>CHECK</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1081,7 +1055,7 @@ location set</source>
<translation>SSH访问权限给您GitHub设置中的所有公钥GitHub用户名comma员工永远不会要求您添加他们的GitHub用户名</translation> <translation>SSH访问权限给您GitHub设置中的所有公钥GitHub用户名comma员工永远不会要求您添加他们的GitHub用户名</translation>
</message> </message>
<message> <message>
<location line="+6"/> <location line="+2"/>
<location line="+24"/> <location line="+24"/>
<source>ADD</source> <source>ADD</source>
<translation></translation> <translation></translation>
@ -1151,7 +1125,7 @@ location set</source>
<context> <context>
<name>TogglesPanel</name> <name>TogglesPanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-324"/> <location filename="../qt/offroad/settings.cc" line="-303"/>
<source>Enable openpilot</source> <source>Enable openpilot</source>
<translation>openpilot</translation> <translation>openpilot</translation>
</message> </message>
@ -1288,12 +1262,11 @@ location set</source>
<name>WifiUI</name> <name>WifiUI</name>
<message> <message>
<location filename="../qt/offroad/networking.cc" line="+113"/> <location filename="../qt/offroad/networking.cc" line="+113"/>
<location line="+53"/>
<source>Scanning for networks...</source> <source>Scanning for networks...</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+26"/> <location line="+80"/>
<source>CONNECTING...</source> <source>CONNECTING...</source>
<translation></translation> <translation></translation>
</message> </message>

@ -193,7 +193,7 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+4"/> <location line="+3"/>
<source>Select a language</source> <source>Select a language</source>
<translation></translation> <translation></translation>
</message> </message>
@ -418,7 +418,7 @@ prime subscription. Sign up now: https://connect.comma.ai</source>
https://connect.comma.ai</translation> https://connect.comma.ai</translation>
</message> </message>
<message> <message>
<location line="+57"/> <location line="+58"/>
<source>No home <source>No home
location set</source> location set</source>
<translation> <translation>
@ -432,7 +432,7 @@ location set</source>
</translation> </translation>
</message> </message>
<message> <message>
<location line="+113"/> <location line="+120"/>
<source>no recent destinations</source> <source>no recent destinations</source>
<translation></translation> <translation></translation>
</message> </message>
@ -718,7 +718,7 @@ location set</source>
<context> <context>
<name>SettingsWindow</name> <name>SettingsWindow</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="+101"/> <location filename="../qt/offroad/settings.cc" line="+22"/>
<source>×</source> <source>×</source>
<translation>×</translation> <translation>×</translation>
</message> </message>
@ -983,68 +983,47 @@ location set</source>
<context> <context>
<name>SoftwarePanel</name> <name>SoftwarePanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-130"/> <location filename="../qt/offroad/software_settings.cc" line="+24"/>
<source>Git Branch</source> <source>Updates are only downloaded while the car is off.</source>
<translation>Git </translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Git Commit</source>
<translation>Git </translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+5"/>
<source>OS Version</source> <source>Current Version</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+4"/>
<source>Version</source> <source>Download</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+1"/> <location line="+12"/>
<source>Last Update Check</source> <source>Install Update</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<source>The last time openpilot successfully checked for an update. The updater only runs while the car is off.</source> <source>INSTALL</source>
<translation></translation> <translation type="unfinished"></translation>
</message>
<message>
<location line="+1"/>
<source>Check for Update</source>
<translation></translation>
</message>
<message>
<location line="+5"/>
<source>CHECKING</source>
<translation></translation>
</message>
<message>
<location line="+7"/>
<source>Switch Branch</source>
<translation></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+8"/>
<source>ENTER</source> <source>Target Branch</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+0"/>
<location line="+2"/> <source>SELECT</source>
<source>The new branch will be pulled the next time the updater runs.</source> <translation type="unfinished"></translation>
<translation></translation>
</message> </message>
<message> <message>
<location line="+0"/> <location line="+13"/>
<source>Enter branch name</source> <source>Select a branch</source>
<translation></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location line="+11"/> <location line="+12"/>
<source>UNINSTALL</source> <source>UNINSTALL</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1059,13 +1038,8 @@ location set</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location line="+17"/> <location line="-47"/>
<source>failed to fetch update</source> <location line="+3"/>
<translation></translation>
</message>
<message>
<location line="+1"/>
<location line="+21"/>
<source>CHECK</source> <source>CHECK</source>
<translation></translation> <translation></translation>
</message> </message>
@ -1083,7 +1057,7 @@ location set</source>
<translation> GitHub SSH GitHub comma GitHub </translation> <translation> GitHub SSH GitHub comma GitHub </translation>
</message> </message>
<message> <message>
<location line="+6"/> <location line="+2"/>
<location line="+24"/> <location line="+24"/>
<source>ADD</source> <source>ADD</source>
<translation></translation> <translation></translation>
@ -1153,7 +1127,7 @@ location set</source>
<context> <context>
<name>TogglesPanel</name> <name>TogglesPanel</name>
<message> <message>
<location filename="../qt/offroad/settings.cc" line="-324"/> <location filename="../qt/offroad/settings.cc" line="-303"/>
<source>Enable openpilot</source> <source>Enable openpilot</source>
<translation> openpilot</translation> <translation> openpilot</translation>
</message> </message>
@ -1290,12 +1264,11 @@ location set</source>
<name>WifiUI</name> <name>WifiUI</name>
<message> <message>
<location filename="../qt/offroad/networking.cc" line="+113"/> <location filename="../qt/offroad/networking.cc" line="+113"/>
<location line="+53"/>
<source>Scanning for networks...</source> <source>Scanning for networks...</source>
<translation>...</translation> <translation>...</translation>
</message> </message>
<message> <message>
<location line="+26"/> <location line="+80"/>
<source>CONNECTING...</source> <source>CONNECTING...</source>
<translation>...</translation> <translation>...</translation>
</message> </message>

@ -23,6 +23,7 @@
# disable this service. # disable this service.
import os import os
import re
import datetime import datetime
import subprocess import subprocess
import psutil import psutil
@ -31,8 +32,9 @@ import signal
import fcntl import fcntl
import time import time
import threading import threading
from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import List, Tuple, Optional from typing import List, Union, Optional
from markdown_it import MarkdownIt from markdown_it import MarkdownIt
from common.basedir import BASEDIR 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 DAYS_NO_CONNECTIVITY_PROMPT = 10 # send an offroad prompt after this many days
class WaitTimeHelper: class WaitTimeHelper:
def __init__(self, proc): def __init__(self):
self.proc = proc
self.ready_event = threading.Event() self.ready_event = threading.Event()
self.only_check_for_update = False
signal.signal(signal.SIGHUP, self.update_now) signal.signal(signal.SIGHUP, self.update_now)
signal.signal(signal.SIGUSR1, self.check_now)
def update_now(self, signum: int, frame) -> None: 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() self.ready_event.set()
def sleep(self, t: float) -> None: def sleep(self, t: float) -> None:
self.ready_event.wait(timeout=t) 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') 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) consistent_file.unlink(missing_ok=True)
os.sync() os.sync()
def parse_release_notes(basedir: str) -> bytes:
def set_params(new_version: bool, failed_count: int, exception: Optional[str]) -> None: try:
params = Params() with open(os.path.join(basedir, "RELEASES.md"), "rb") as f:
r = f.read().split(b'\n\n', 1)[0] # Slice latest release notes
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:
try: try:
with open(os.path.join(FINALIZED, "RELEASES.md"), "rb") as f: return bytes(MarkdownIt().render(r.decode("utf-8")), encoding="utf-8")
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")
except Exception: except Exception:
params.put("ReleaseNotes", "") return r + b"\n"
params.put_bool("UpdateAvailable", True) except FileNotFoundError:
pass
# Handle user prompt except Exception:
for alert in ("Offroad_UpdateFailed", "Offroad_ConnectivityNeeded", "Offroad_ConnectivityNeededPrompt"): cloudlog.exception("failed to parse release notes")
set_offroad_alert(alert, False) return b""
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'}.")
def setup_git_options(cwd: str) -> None: def setup_git_options(cwd: str) -> None:
# We sync FS object atimes (which NEOS doesn't use) and mtimes, but ctimes # 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) 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]: if exception is None:
setup_git_options(OVERLAY_MERGED) self.params.remove("LastUpdateException")
try: else:
git_fetch_output = run(["git", "fetch", "--dry-run"], OVERLAY_MERGED) self.params.put("LastUpdateException", exception)
return True, check_git_fetch_result(git_fetch_output)
except subprocess.CalledProcessError:
return False, False
def fetch_update() -> bool: # Write out current and new version info
cloudlog.info("attempting git fetch inside staging overlay") 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<commit_sha>\b[0-9a-f]{5,40}\b)(\s+)(refs\/heads\/)(?P<branch_name>.*$)'
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) self.params.put("UpdaterState", "downloading...")
cloudlog.info("git fetch success: %s", git_fetch_output)
cur_hash = run(["git", "rev-parse", "HEAD"], OVERLAY_MERGED).rstrip() # TODO: cleanly interrupt this and invalidate old update
upstream_hash = run(["git", "rev-parse", "@{u}"], OVERLAY_MERGED).rstrip() set_consistent_flag(False)
new_version: bool = cur_hash != upstream_hash self.params.put_bool("UpdateAvailable", False)
git_fetch_result = check_git_fetch_result(git_fetch_output)
new_branch = Params().get("SwitchToBranch", encoding='utf8') setup_git_options(OVERLAY_MERGED)
if new_branch is not None:
new_version = True
cloudlog.info(f"comparing {cur_hash} to {upstream_hash}") branch = self.target_branch
if new_version or git_fetch_result: git_fetch_output = run(["git", "fetch", "origin", branch], OVERLAY_MERGED)
cloudlog.info("Running update") cloudlog.info("git fetch success: %s", git_fetch_output)
if new_version: cloudlog.info("git reset in progress")
cloudlog.info("git reset in progress") cmds = [
cmds = [ ["git", "checkout", "--force", "--no-recurse-submodules", branch],
["git", "reset", "--hard", "@{u}"], ["git", "reset", "--hard", f"origin/{branch}"],
["git", "clean", "-xdf"], ["git", "clean", "-xdf"],
["git", "submodule", "init"], ["git", "submodule", "init"],
["git", "submodule", "update"], ["git", "submodule", "update"],
] ]
if new_branch is not None: r = [run(cmd, OVERLAY_MERGED) for cmd in cmds]
cloudlog.info(f"switching to branch {repr(new_branch)}") cloudlog.info("git reset success: %s", '\n'.join(r))
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))
if AGNOS: # TODO: show agnos download progress
handle_agnos_update() if AGNOS:
handle_agnos_update()
# Create the finalized, ready-to-swap update # Create the finalized, ready-to-swap update
self.params.put("UpdaterState", "finalizing update...")
finalize_update() finalize_update()
cloudlog.info("openpilot update successful!") cloudlog.info("finalize success!")
else:
cloudlog.info("nothing new from git at this time")
return new_version
def main() -> None: def main() -> None:
@ -357,8 +422,12 @@ def main() -> None:
overlay_init = Path(os.path.join(BASEDIR, ".overlay_init")) overlay_init = Path(os.path.join(BASEDIR, ".overlay_init"))
overlay_init.unlink(missing_ok=True) overlay_init.unlink(missing_ok=True)
updater = Updater()
update_failed_count = 0 # TODO: Load from param? 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 # Run the update loop
while True: while True:
@ -366,21 +435,24 @@ def main() -> None:
# Attempt an update # Attempt an update
exception = None exception = None
new_version = False
update_failed_count += 1
try: try:
# TODO: reuse overlay from previous updated instance if it looks clean
init_overlay() init_overlay()
# TODO: still needed? skip this and just fetch? # ensure we have some params written soon after startup
# Lightweight internt check updater.set_params(update_failed_count, exception)
internet_ok, update_available = check_for_update() update_failed_count += 1
if internet_ok and not update_available:
update_failed_count = 0 # check for update
params.put("UpdaterState", "checking...")
updater.check_for_update()
# Fetch update # download update
if internet_ok: if wait_helper.only_check_for_update:
new_version = fetch_update() cloudlog.info("skipping fetch this cycle")
update_failed_count = 0 else:
updater.fetch_update()
update_failed_count = 0
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
cloudlog.event( cloudlog.event(
"update process failed", "update process failed",
@ -396,12 +468,14 @@ def main() -> None:
overlay_init.unlink(missing_ok=True) overlay_init.unlink(missing_ok=True)
try: try:
set_params(new_version, update_failed_count, exception) params.put("UpdaterState", "idle")
updater.set_params(update_failed_count, exception)
except Exception: except Exception:
cloudlog.exception("uncaught updated exception while setting params, shouldn't happen") cloudlog.exception("uncaught updated exception while setting params, shouldn't happen")
# infrequent attempts if we successfully updated recently # 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__": if __name__ == "__main__":

Loading…
Cancel
Save