diff --git a/selfdrive/frogpilot/controls/lib/frogpilot_functions.py b/selfdrive/frogpilot/controls/lib/frogpilot_functions.py index 248a65ea4..d81649894 100644 --- a/selfdrive/frogpilot/controls/lib/frogpilot_functions.py +++ b/selfdrive/frogpilot/controls/lib/frogpilot_functions.py @@ -1,4 +1,6 @@ +import datetime import filecmp +import glob import http.client import os import shutil @@ -11,7 +13,8 @@ import urllib.request from openpilot.common.basedir import BASEDIR from openpilot.common.numpy_fast import clip, interp, mean -from openpilot.common.params_pyx import Params, UnknownKeyName +from openpilot.common.params_pyx import Params, ParamKeyType, UnknownKeyName +from openpilot.common.time import system_time_valid from openpilot.system.hardware import HARDWARE def delete_file(file): @@ -47,6 +50,45 @@ def run_cmd(cmd, success_msg, fail_msg): except Exception as e: print(f"Unexpected error occurred: {e}") +def backup_directory(backup, destination, success_msg, fail_msg): + os.makedirs(destination, exist_ok=True) + try: + run_cmd(['sudo', 'cp', '-a', os.path.join(backup, '.'), destination], success_msg, fail_msg) + except OSError as e: + if e.errno == 28: + print("Not enough space to perform the backup.") + else: + print(f"Failed to backup due to unexpected error: {e}") + +def cleanup_backups(directory, limit): + backups = sorted(glob.glob(os.path.join(directory, "*_auto")), key=os.path.getmtime, reverse=True) + for old_backup in backups[limit:]: + subprocess.run(['sudo', 'rm', '-rf', old_backup], check=True) + print(f"Deleted oldest backup: {os.path.basename(old_backup)}") + +def backup_frogpilot(build_metadata): + backup_path = "/data/backups" + cleanup_backups(backup_path, 4) + + branch = build_metadata.channel + commit = build_metadata.openpilot.git_commit_date[12:-16] + + backup_dir = f"{backup_path}/{branch}_{commit}_auto" + backup_directory(BASEDIR, backup_dir, f"Successfully backed up FrogPilot to {backup_dir}.", f"Failed to backup FrogPilot to {backup_dir}.") + +def backup_toggles(params, params_storage): + for key in params.all_keys(): + if params.get_key_type(key) & ParamKeyType.FROGPILOT_STORAGE: + value = params.get(key) + if value is not None: + params_storage.put(key, value) + + backup_path = "/data/toggle_backups" + cleanup_backups(backup_path, 9) + + backup_dir = f"{backup_path}/{datetime.datetime.now().strftime('%Y-%m-%d_%I-%M%p').lower()}_auto" + backup_directory("/data/params/d", backup_dir, f"Successfully backed up toggles to {backup_dir}.", f"Failed to backup toggles to {backup_dir}.") + def convert_params(params, params_storage): def convert_param(key, action_func): try: @@ -71,9 +113,19 @@ def convert_params(params, params_storage): print("Params successfully converted!") params_storage.put_int_nonblocking("ParamConversionVersion", version) -def frogpilot_boot_functions(params, params_storage): +def frogpilot_boot_functions(build_metadata, params, params_storage): convert_params(params, params_storage) + while not system_time_valid(): + print("Waiting for system time to become valid...") + time.sleep(1) + + try: + backup_frogpilot(build_metadata) + backup_toggles(params, params_storage) + except subprocess.CalledProcessError as e: + print(f"Backup failed: {e}") + def setup_frogpilot(build_metadata): remount_root = ['sudo', 'mount', '-o', 'remount,rw', '/'] run_cmd(remount_root, "File system remounted as read-write.", "Failed to remount file system.") diff --git a/selfdrive/frogpilot/frogpilot_process.py b/selfdrive/frogpilot/frogpilot_process.py index 07e087880..b17219f99 100644 --- a/selfdrive/frogpilot/frogpilot_process.py +++ b/selfdrive/frogpilot/frogpilot_process.py @@ -9,12 +9,13 @@ from openpilot.common.time import system_time_valid from openpilot.system.hardware import HARDWARE from openpilot.selfdrive.frogpilot.controls.frogpilot_planner import FrogPilotPlanner -from openpilot.selfdrive.frogpilot.controls.lib.frogpilot_functions import is_url_pingable +from openpilot.selfdrive.frogpilot.controls.lib.frogpilot_functions import backup_toggles, is_url_pingable from openpilot.selfdrive.frogpilot.controls.lib.frogpilot_variables import FrogPilotVariables OFFLINE = log.DeviceState.NetworkType.none locks = { + "backup_toggles": threading.Lock(), "time_checks": threading.Lock(), "update_frogpilot_params": threading.Lock(), } @@ -58,6 +59,7 @@ def frogpilot_thread(): params = Params() params_memory = Params("/dev/shm/params") + params_storage = Params("/persist/params") frogpilot_planner = FrogPilotPlanner() @@ -91,6 +93,9 @@ def frogpilot_thread(): elif update_toggles: run_thread_with_lock("update_frogpilot_params", locks["update_frogpilot_params"], FrogPilotVariables.update_frogpilot_params, (started,)) + if time_validated and not started: + run_thread_with_lock("backup_toggles", locks["backup_toggles"], backup_toggles, (params, params_storage)) + update_toggles = False started_previously = started diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 53ac3c572..b3540e189 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include @@ -274,8 +276,172 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { btn->setEnabled(offroad); } } + for (FrogPilotButtonsControl *btn : findChildren()) { + btn->setEnabled(offroad); + } }); + // Backup FrogPilot + std::vector frogpilotBackupOptions{tr("BACKUP"), tr("DELETE"), tr("RESTORE")}; + FrogPilotButtonsControl *frogpilotBackupBtn = new FrogPilotButtonsControl(tr("FrogPilot Backups"), tr("Backup, delete, or restore your FrogPilot backups."), "", frogpilotBackupOptions); + connect(frogpilotBackupBtn, &FrogPilotButtonsControl::buttonClicked, [=](int id) { + QDir backupDir("/data/backups"); + + if (id == 0) { + QString nameSelection = InputDialog::getText(tr("Name your backup"), this, "", false, 1); + if (!nameSelection.isEmpty()) { + std::thread([=]() { + frogpilotBackupBtn->setEnabled(false); + frogpilotBackupBtn->setValue(tr("Backing up...")); + + std::string fullBackupPath = backupDir.absolutePath().toStdString() + "/" + nameSelection.toStdString(); + std::string command = "mkdir -p " + fullBackupPath + " && rsync -av /data/openpilot/ " + fullBackupPath + "/"; + + int result = std::system(command.c_str()); + frogpilotBackupBtn->setValue(result == 0 ? tr("Success!") : tr("Failed...")); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + frogpilotBackupBtn->setValue(""); + frogpilotBackupBtn->setEnabled(true); + }).detach(); + } + + } else if (id == 1) { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a backup to delete"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to delete this backup?"), tr("Delete"), this)) return; + std::thread([=]() { + frogpilotBackupBtn->setEnabled(false); + frogpilotBackupBtn->setValue(tr("Deleting...")); + + QDir dirToDelete(backupDir.absoluteFilePath(selection)); + + frogpilotBackupBtn->setValue(dirToDelete.removeRecursively() ? tr("Deleted!") : tr("Failed...")); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + frogpilotBackupBtn->setValue(""); + frogpilotBackupBtn->setEnabled(true); + }).detach(); + } + + } else if (id == 2) { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a restore point"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to restore this version of FrogPilot?"), tr("Restore"), this)) return; + std::thread([=]() { + frogpilotBackupBtn->setEnabled(false); + frogpilotBackupBtn->setValue(tr("Restoring...")); + + std::string sourcePath = backupDir.absolutePath().toStdString() + "/" + selection.toStdString(); + std::string targetPath = "/data/safe_staging/finalized"; + std::string consistentFilePath = targetPath + "/.overlay_consistent"; + std::string command = "rsync -av --delete -l --exclude='.overlay_consistent' " + sourcePath + "/ " + targetPath + "/"; + int result = std::system(command.c_str()); + + if (result == 0) { + std::ofstream consistentFile(consistentFilePath); + if (consistentFile) { + frogpilotBackupBtn->setValue(tr("Restored!")); + std::this_thread::sleep_for(std::chrono::seconds(2)); + frogpilotBackupBtn->setValue(tr("Rebooting...")); + std::this_thread::sleep_for(std::chrono::seconds(2)); + consistentFile.close(); + Hardware::reboot(); + } else { + frogpilotBackupBtn->setValue(tr("Failed...")); + } + } else { + frogpilotBackupBtn->setValue(tr("Failed...")); + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + frogpilotBackupBtn->setValue(""); + frogpilotBackupBtn->setEnabled(true); + }).detach(); + } + } + }); + addItem(frogpilotBackupBtn); + + // Backup toggles + std::vector toggleBackupOptions{tr("BACKUP"), tr("DELETE"), tr("RESTORE")}; + FrogPilotButtonsControl *toggleBackupBtn = new FrogPilotButtonsControl(tr("Toggle Backups"), tr("Backup, delete, or restore your toggle backups."), "", toggleBackupOptions); + connect(toggleBackupBtn, &FrogPilotButtonsControl::buttonClicked, [=](int id) { + QDir backupDir("/data/toggle_backups"); + + if (id == 0) { + QString nameSelection = InputDialog::getText(tr("Name your backup"), this, "", false, 1); + if (!nameSelection.isEmpty()) { + std::thread([=]() { + toggleBackupBtn->setEnabled(false); + toggleBackupBtn->setValue(tr("Backing up...")); + + std::string fullBackupPath = backupDir.absolutePath().toStdString() + "/" + nameSelection.toStdString() + "/"; + std::string command = "mkdir -p " + fullBackupPath + " && rsync -av /data/params/d/ " + fullBackupPath; + + int result = std::system(command.c_str()); + toggleBackupBtn->setValue(result == 0 ? tr("Success!") : tr("Failed...")); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + toggleBackupBtn->setValue(""); + toggleBackupBtn->setEnabled(true); + }).detach(); + } + + } else if (id == 1) { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a backup to delete"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to delete this backup?"), tr("Delete"), this)) return; + std::thread([=]() { + toggleBackupBtn->setEnabled(false); + toggleBackupBtn->setValue(tr("Deleting...")); + + QDir dirToDelete(backupDir.absoluteFilePath(selection)); + + toggleBackupBtn->setValue(dirToDelete.removeRecursively() ? tr("Deleted!") : tr("Failed...")); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + toggleBackupBtn->setValue(""); + toggleBackupBtn->setEnabled(true); + }).detach(); + } + + } else if (id == 2) { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a restore point"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to restore this toggle backup?"), tr("Restore"), this)) return; + std::thread([=]() { + toggleBackupBtn->setEnabled(false); + toggleBackupBtn->setValue(tr("Restoring...")); + + std::string sourcePath = backupDir.absolutePath().toStdString() + "/" + selection.toStdString() + "/"; + std::string targetPath = "/data/params/d/"; + std::string command = "rsync -av --delete -l " + sourcePath + " " + targetPath; + int result = std::system(command.c_str()); + + if (result == 0) { + toggleBackupBtn->setValue(tr("Success!")); + updateFrogPilotToggles(); + } else { + toggleBackupBtn->setValue(tr("Failed...")); + } + + std::this_thread::sleep_for(std::chrono::seconds(2)); + toggleBackupBtn->setValue(""); + toggleBackupBtn->setEnabled(true); + }).detach(); + } + } + }); + addItem(toggleBackupBtn); + // power buttons QHBoxLayout *power_layout = new QHBoxLayout(); power_layout->setSpacing(30); diff --git a/system/manager/manager.py b/system/manager/manager.py index 09f88b823..59424cbf4 100644 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -37,7 +37,7 @@ def manager_init() -> None: if build_metadata.release_channel: params.clear_all(ParamKeyType.DEVELOPMENT_ONLY) - threading.Thread(target=frogpilot_boot_functions, args=(params, params_storage,)).start() + threading.Thread(target=frogpilot_boot_functions, args=(build_metadata, params, params_storage,)).start() default_params: list[tuple[str, str | bytes]] = [ ("AlwaysOnDM", "0"),