diff --git a/common/params.cc b/common/params.cc index 551d22f..b5886e9 100644 --- a/common/params.cc +++ b/common/params.cc @@ -223,6 +223,8 @@ std::unordered_map keys = { {"LongitudinalTune", PERSISTENT}, {"OfflineMode", PERSISTENT}, {"Updated", PERSISTENT}, + {"UpdateSchedule", PERSISTENT}, + {"UpdateTime", PERSISTENT}, }; } // namespace diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index c5291ee..b4f89e9 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -103,4 +103,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 15c022d..15a4687 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 @@ -29,6 +31,40 @@ 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")}; + ButtonParamControl *preferredSchedule = new ButtonParamControl("UpdateSchedule", tr("Update Scheduler"), + tr("Choose the update frequency for FrogPilot's automatic updates.\n\n" + "This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' experience.\n\n" + "Weekly updates start at midnight every Sunday."), + "", + scheduleOptions); + schedule = params.getInt("UpdateSchedule"); + 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]); + addItem(updateTime); + // download update btn downloadBtn = new ButtonControl(tr("Download"), tr("CHECK")); connect(downloadBtn, &ButtonControl::clicked, [=]() { @@ -85,6 +121,8 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { fs_watch = new ParamWatcher(this); QObject::connect(fs_watch, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + schedule = params.getInt("UpdateSchedule"); + time = params.getInt("UpdateTime"); updateLabels(); }); @@ -93,6 +131,8 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { updateLabels(); }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &SoftwarePanel::automaticUpdate); + updateLabels(); } @@ -110,6 +150,9 @@ void SoftwarePanel::updateLabels() { fs_watch->addParam("UpdaterState"); fs_watch->addParam("UpdateAvailable"); + fs_watch->addParam("UpdateSchedule"); + fs_watch->addParam("UpdateTime"); + if (!isVisible()) { return; } @@ -152,5 +195,78 @@ 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() { + static int timer = 0; + static std::chrono::system_clock::time_point lastOffroadTime; + + if (!is_onroad) { + timer = (timer == 0) ? 0 : std::chrono::duration_cast(std::chrono::system_clock::now() - lastOffroadTime).count(); + lastOffroadTime = std::chrono::system_clock::now(); + } else { + timer = 0; + } + + bool isWifiConnected = (*uiState()->sm)["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; + if (schedule == 0 || is_onroad || !isWifiConnected || isVisible()) return; + + static bool isDownloadCompleted = false; + if (isDownloadCompleted && params.getBool("UpdateAvailable")) { + params.putBool(true); + return; + } + + int updateHour = time / 2; + int updateMinute = (time % 2) * 30; + + if (updateHour >= 1 && updateHour <= 11 && time >= 24) { + updateHour += 12; + } else if (updateHour == 12 && time < 24) { + updateHour = 0; + } + + std::time_t currentTimeT = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm now = *std::localtime(¤tTimeT); + + static bool updateCheckedToday = false; + static int lastCheckedDay = now.tm_yday; + if (lastCheckedDay != now.tm_yday) { + updateCheckedToday = false; + lastCheckedDay = now.tm_yday; + } + + if (now.tm_hour != updateHour || now.tm_min < updateMinute) return; + + std::string lastUpdateStr = params.get("Updated"); + std::tm lastUpdate = {}; + std::istringstream iss(lastUpdateStr); + + if (iss >> std::get_time(&lastUpdate, "%Y-%m-%d %H:%M:%S")) { + lastUpdate.tm_year -= 1900; + lastUpdate.tm_mon -= 1; + } + std::time_t lastUpdateTimeT = std::mktime(&lastUpdate); + + if (lastUpdate.tm_yday == now.tm_yday) { + return; + } + + if (!isDownloadCompleted) { + 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 index a8ac2d6..2796690 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -504,7 +504,7 @@ def main() -> None: # infrequent attempts if we successfully updated recently wait_helper.only_check_for_update = False - wait_helper.sleep(5*60 if update_failed_count > 0 else 1.5*60*60) + wait_helper.sleep(5*60 if update_failed_count > 0 else (1.5*60*60 if params.get_int("UpdateSchedule") else 100*365*24*60*60)) if __name__ == "__main__":