diff --git a/common/params.cc b/common/params.cc index d7d8cb2a..e262ad66 100644 --- a/common/params.cc +++ b/common/params.cc @@ -226,6 +226,7 @@ std::unordered_map keys = { {"FrogsGoMoo", PERSISTENT}, {"LateralTune", PERSISTENT}, {"LongitudinalTune", PERSISTENT}, + {"ManualUpdateInitiated", CLEAR_ON_MANAGER_START}, {"PromptVolume", PERSISTENT}, {"PromptDistractedVolume", PERSISTENT}, {"QOLControls", PERSISTENT}, @@ -234,6 +235,8 @@ std::unordered_map keys = { {"SilentMode", PERSISTENT}, {"StockTune", PERSISTENT}, {"StorageParamsSet", PERSISTENT}, + {"UpdateSchedule", PERSISTENT}, + {"UpdateTime", PERSISTENT}, {"WarningSoftVolume", PERSISTENT}, {"WarningImmediateVolume", PERSISTENT}, }; diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 87b21239..3e4987f1 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -106,4 +106,12 @@ private: Params params; ParamWatcher *fs_watch; + + // FrogPilot variables + void automaticUpdate(); + + ButtonControl *updateTime; + + int schedule; + int time; }; diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc index b44dc48d..22988e5e 100644 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -30,11 +32,54 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { versionLbl = new LabelControl(tr("Current Version"), ""); addItem(versionLbl); + // Update scheduler + std::vector scheduleOptions{tr("Manually"), tr("Daily"), tr("Weekly")}; + FrogPilotButtonParamControl *preferredSchedule = new FrogPilotButtonParamControl("UpdateSchedule", tr("Update Scheduler"), + tr("Choose the frequency to automatically update FrogPilot.\n\n" + "This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience.\n\n" + "Weekly updates start at midnight every Sunday."), + "", + scheduleOptions); + schedule = params.getInt("UpdateSchedule"); + QObject::connect(preferredSchedule, &FrogPilotButtonParamControl::buttonClicked, [this](int id) { + schedule = id; + updateLabels(); + }); + addItem(preferredSchedule); + + updateTime = new ButtonControl(tr("Update Time"), tr("SELECT")); + QStringList hours; + for (int h = 0; h < 24; h++) { + int displayHour = (h % 12 == 0) ? 12 : h % 12; + QString meridiem = (h < 12) ? "AM" : "PM"; + hours << QString("%1:00 %2").arg(displayHour).arg(meridiem) + << QString("%1:30 %2").arg(displayHour).arg(meridiem); + } + + QObject::connect(updateTime, &ButtonControl::clicked, [=]() { + int currentHourIndex = params.getInt("UpdateTime"); + QString currentHourLabel = hours[currentHourIndex]; + + QString selection = MultiOptionDialog::getSelection(tr("Select a time to automatically update"), hours, currentHourLabel, this); + if (!selection.isEmpty()) { + int selectedHourIndex = hours.indexOf(selection); + params.putInt("UpdateTime", selectedHourIndex); + updateTime->setValue(selection); + } + }); + time = params.getInt("UpdateTime"); + updateTime->setValue(hours[time]); + updateTime->setVisible(schedule != 0); + addItem(updateTime); + // download update btn downloadBtn = new ButtonControl(tr("Download"), tr("CHECK")); connect(downloadBtn, &ButtonControl::clicked, [=]() { downloadBtn->setEnabled(false); if (downloadBtn->text() == tr("CHECK")) { + if (schedule == 0) { + params.putBool("ManualUpdateInitiated", true); + } checkForUpdates(); } else { std::system("pkill -SIGHUP -f selfdrive.updated"); @@ -94,6 +139,8 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { updateLabels(); }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &SoftwarePanel::automaticUpdate); + updateLabels(); } @@ -126,7 +173,7 @@ void SoftwarePanel::updateLabels() { downloadBtn->setEnabled(false); downloadBtn->setValue(updater_state); } else { - if (failed) { + if (failed && schedule != 0) { downloadBtn->setText(tr("CHECK")); downloadBtn->setValue(tr("failed to check for update")); } else if (params.getBool("UpdaterFetchAvailable")) { @@ -153,5 +200,55 @@ void SoftwarePanel::updateLabels() { installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription"))); installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes"))); + updateTime->setVisible(params.getInt("UpdateSchedule")); + update(); } + +void SoftwarePanel::automaticUpdate() { + // Variable declarations + static bool isDownloadCompleted = false; + static bool updateCheckedToday = false; + + std::time_t currentTimeT = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm now = *std::localtime(¤tTimeT); + + // Check to make sure we're not onroad and have a WiFi connection + bool isWifiConnected = (*uiState()->sm)["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; + if (schedule == 0 || is_onroad || !isWifiConnected || isVisible()) return; + + // Reboot if an automatic update was completed + if (isDownloadCompleted) { + if (installBtn->isVisible()) Hardware::reboot(); + return; + } + + // Format "Updated" to a useable format + std::tm lastUpdate; + std::istringstream ss(params.get("Updated")); + ss >> std::get_time(&lastUpdate, "%Y-%m-%d %H:%M:%S"); + std::time_t lastUpdateTimeT = std::mktime(&lastUpdate); + + // Check if an update was already performed today + static int lastCheckedDay = now.tm_yday; + if (lastCheckedDay != now.tm_yday) { + updateCheckedToday = false; + lastCheckedDay = now.tm_yday; + } else if (lastUpdate.tm_yday == now.tm_yday) { + return; + } + + // Check if it's time to update + std::chrono::hours durationSinceLastUpdate = std::chrono::duration_cast(std::chrono::system_clock::now() - std::chrono::system_clock::from_time_t(lastUpdateTimeT)); + int daysSinceLastUpdate = durationSinceLastUpdate.count() / 24; + + if ((schedule == 1 && daysSinceLastUpdate >= 1) || (schedule == 2 && (now.tm_yday / 7) != (std::localtime(&lastUpdateTimeT)->tm_yday / 7))) { + if (downloadBtn->text() == tr("CHECK") && !updateCheckedToday) { + checkForUpdates(); + updateCheckedToday = true; + } else { + std::system("pkill -SIGHUP -f selfdrive.updated"); + isDownloadCompleted = true; + } + } +} diff --git a/selfdrive/updated.py b/selfdrive/updated.py old mode 100755 new mode 100644 index c1f85369..b95bf0d1 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -468,21 +468,22 @@ def main() -> None: update_failed_count += 1 # check for update - params.put("UpdaterState", "checking...") - updater.check_for_update() + if params.get_int("UpdateSchedule") != 0 or params.get_bool("ManualUpdateInitiated"): + params.put("UpdaterState", "checking...") + updater.check_for_update() - # download update - last_fetch = read_time_from_param(params, "UpdaterLastFetchTime") - timed_out = last_fetch is None or (datetime.datetime.utcnow() - last_fetch > datetime.timedelta(days=3)) - user_requested_fetch = wait_helper.user_request == UserRequest.FETCH - if params.get_bool("NetworkMetered") and not timed_out and not user_requested_fetch: - cloudlog.info("skipping fetch, connection metered") - elif wait_helper.user_request == UserRequest.CHECK: - cloudlog.info("skipping fetch, only checking") - else: - updater.fetch_update() - write_time_to_param(params, "UpdaterLastFetchTime") - update_failed_count = 0 + # download update + last_fetch = read_time_from_param(params, "UpdaterLastFetchTime") + timed_out = last_fetch is None or (datetime.datetime.utcnow() - last_fetch > datetime.timedelta(days=3)) + user_requested_fetch = wait_helper.user_request == UserRequest.FETCH + if params.get_bool("NetworkMetered") and not timed_out and not user_requested_fetch: + cloudlog.info("skipping fetch, connection metered") + elif wait_helper.user_request == UserRequest.CHECK: + cloudlog.info("skipping fetch, only checking") + else: + updater.fetch_update() + write_time_to_param(params, "UpdaterLastFetchTime") + update_failed_count = 0 except subprocess.CalledProcessError as e: cloudlog.event( "update process failed",