From 4eec6306a98c71ed2412f2da69c0f248515d8db9 Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Mon, 26 Apr 2021 12:07:47 +0200 Subject: [PATCH] Add upload progress display in developer, bugfix on file download in developer --- include/ui/backgroundworker_ui.py | 50 ++++++++++++++ include/ui_dev/backgroundworker.ui | 54 +++++++++++++++ revpicommander/backgroundworker.py | 103 +++++++++++++++++++++++++++++ revpicommander/revpifiles.py | 100 +++++++++++++++++----------- 4 files changed, 269 insertions(+), 38 deletions(-) create mode 100644 include/ui/backgroundworker_ui.py create mode 100644 include/ui_dev/backgroundworker.ui create mode 100644 revpicommander/backgroundworker.py diff --git a/include/ui/backgroundworker_ui.py b/include/ui/backgroundworker_ui.py new file mode 100644 index 0000000..15b9fb5 --- /dev/null +++ b/include/ui/backgroundworker_ui.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'backgroundworker.ui' +# +# Created by: PyQt5 UI code generator 5.14.1 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_diag_backgroundworker(object): + def setupUi(self, diag_backgroundworker): + diag_backgroundworker.setObjectName("diag_backgroundworker") + diag_backgroundworker.resize(418, 97) + diag_backgroundworker.setModal(True) + self.verticalLayout = QtWidgets.QVBoxLayout(diag_backgroundworker) + self.verticalLayout.setObjectName("verticalLayout") + self.lbl_status = QtWidgets.QLabel(diag_backgroundworker) + self.lbl_status.setText("Status message...") + self.lbl_status.setObjectName("lbl_status") + self.verticalLayout.addWidget(self.lbl_status) + self.pgb_status = QtWidgets.QProgressBar(diag_backgroundworker) + self.pgb_status.setMinimumSize(QtCore.QSize(400, 0)) + self.pgb_status.setObjectName("pgb_status") + self.verticalLayout.addWidget(self.pgb_status) + self.btn_box = QtWidgets.QDialogButtonBox(diag_backgroundworker) + self.btn_box.setOrientation(QtCore.Qt.Horizontal) + self.btn_box.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel) + self.btn_box.setCenterButtons(True) + self.btn_box.setObjectName("btn_box") + self.verticalLayout.addWidget(self.btn_box) + + self.retranslateUi(diag_backgroundworker) + QtCore.QMetaObject.connectSlotsByName(diag_backgroundworker) + + def retranslateUi(self, diag_backgroundworker): + _translate = QtCore.QCoreApplication.translate + diag_backgroundworker.setWindowTitle(_translate("diag_backgroundworker", "File transfer...")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + diag_backgroundworker = QtWidgets.QDialog() + ui = Ui_diag_backgroundworker() + ui.setupUi(diag_backgroundworker) + diag_backgroundworker.show() + sys.exit(app.exec_()) diff --git a/include/ui_dev/backgroundworker.ui b/include/ui_dev/backgroundworker.ui new file mode 100644 index 0000000..ceedc0a --- /dev/null +++ b/include/ui_dev/backgroundworker.ui @@ -0,0 +1,54 @@ + + + diag_backgroundworker + + + + 0 + 0 + 418 + 97 + + + + File transfer... + + + true + + + + + + Status message... + + + + + + + + 400 + 0 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + true + + + + + + + + diff --git a/revpicommander/backgroundworker.py b/revpicommander/backgroundworker.py new file mode 100644 index 0000000..a12036d --- /dev/null +++ b/revpicommander/backgroundworker.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +"""File transfer system to handle QThreads.""" +__author__ = "Sven Sager" +__copyright__ = "Copyright (C) 2021 Sven Sager" +__license__ = "GPLv3" + +from logging import getLogger + +from PyQt5 import QtCore, QtGui, QtWidgets + +from ui.backgroundworker_ui import Ui_diag_backgroundworker + +log = getLogger() + + +class BackgroundWorker(QtCore.QThread): + steps_todo = QtCore.pyqtSignal(int) + steps_done = QtCore.pyqtSignal(int) + status_message = QtCore.pyqtSignal(str) + + def __init__(self, parent=None): + super(BackgroundWorker, self).__init__(parent) + + def check_cancel(self) -> bool: + """ + Check for interruption of thread and show message + + :return: True, if interruption was requested + """ + if self.isInterruptionRequested(): + self.status_message.emit(self.tr("User requested cancellation...")) + self.msleep(750) + return True + return False + + def exec_dialog(self) -> int: + diag = WorkerDialog(self, self.parent()) + rc = diag.exec() + diag.deleteLater() + return rc + + def wait_interruptable(self, seconds=-1) -> None: + """Save function to wait and get the cancel buttons.""" + counter = seconds * 4 + while counter != 0: + counter -= 1 + self.msleep(250) + if self._check_cancel(): + break + + def run(self) -> None: + """Worker thread to import pictures from camera.""" + log.debug("BackgroundWorker.run") + self.status_message.emit("Started dummy thread...") + self.wait_interruptable(5) + self.status_message.emit("Completed dummy thread.") + self._save_wait(3) + + +class WorkerDialog(QtWidgets.QDialog, Ui_diag_backgroundworker): + + def __init__(self, worker_thread: BackgroundWorker, parent=None): + """ + Base of dialog to show progress from a background thread. + + :param worker_thread: Thread with the logic work to do + :param parent: QtWidget + """ + super(WorkerDialog, self).__init__(parent) + self.setupUi(self) + + self._canceled = False + + self._th = worker_thread + self._th.finished.connect(self.on_th_finished) + self._th.steps_todo.connect(self.pgb_status.setMaximum) + self._th.steps_done.connect(self.pgb_status.setValue) + self._th.status_message.connect(self.lbl_status.setText) + + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + a0.ignore() + + def exec(self) -> int: + self._th.start() + return super(WorkerDialog, self).exec() + + @QtCore.pyqtSlot() + def on_th_finished(self) -> None: + """Check the result of import thread.""" + if self._canceled: + self.reject() + else: + self.accept() + + @QtCore.pyqtSlot(QtWidgets.QAbstractButton) + def on_btn_box_clicked(self, button: QtWidgets.QAbstractButton) -> None: + """Control buttons for dialog.""" + role = self.btn_box.buttonRole(button) + log.debug("WorkerDialog.on_btn_box_clicked({0})".format(role)) + + if role == QtWidgets.QDialogButtonBox.RejectRole: + self._th.requestInterruption() + self._canceled = True diff --git a/revpicommander/revpifiles.py b/revpicommander/revpifiles.py index f3f234c..f0b97f9 100644 --- a/revpicommander/revpifiles.py +++ b/revpicommander/revpifiles.py @@ -13,6 +13,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets import helper import proginit as pi +from backgroundworker import BackgroundWorker from helper import WidgetData from ui.files_ui import Ui_win_files @@ -22,6 +23,57 @@ class NodeType(IntEnum): DIR = 1001 +class UploadFiles(BackgroundWorker): + + def __init__(self, file_list: list, parent): + super(UploadFiles, self).__init__(parent) + self.ec = 1 + self.file_list = file_list + self.plc_program_included = False # Will be True, when opt_program was found in files + + def run(self) -> None: + self.steps_todo.emit(len(self.file_list)) + + # Get config to find actual auto start program for warnings + opt_program = helper.cm.call_remote_function("get_config", default_value={}) + opt_program = opt_program.get("plcprogram", "none.py") + + progress_counter = 0 + for file_name in self.file_list: + progress_counter += 1 + + # Remove base dir of file to set relative for PyLoad + send_name = file_name.replace(helper.cm.develop_watch_path, "")[1:] + self.status_message.emit(send_name) + + # Check whether this is the auto start program + if send_name == opt_program: + self.plc_program_included = True + + + # Transfer file + try: + with open(file_name, "rb") as fh: + upload_status = helper.cm.call_remote_function( + "plcupload", Binary(gzip.compress(fh.read())), send_name, + default_value=False + ) + except Exception as e: + pi.logger.error(e) + self.ec = -2 + return + + if not upload_status: + self.ec = -1 + return + + self.steps_done.emit(progress_counter) + if self.check_cancel(): + return + + self.ec = 0 + + class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files): def __init__(self, parent=None): @@ -72,41 +124,13 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files): ) return - # Get config to find actual auto start program for warnings - opt_program = helper.cm.call_remote_function("get_config", default_value={}) - opt_program = opt_program.get("plcprogram", "none.py") - uploaded = True # Will be False, when opt_program was found in files - ec = 0 + uploader = UploadFiles(self.file_list_local(), self) + if uploader.exec_dialog() == QtWidgets.QDialog.Rejected: + return - # todo: Do this in a thread with status bar to prevent freezing program on long upload times - for file_name in self.file_list_local(): - # todo: Check exception of local file - with open(file_name, "rb") as fh: - # Remove base dir of file to set relative for PyLoad - send_name = file_name.replace(helper.cm.develop_watch_path, "")[1:] - - # Check whether this is the auto start program - if send_name == opt_program: - uploaded = False - - # Transfer file - try: - upload_status = helper.cm.call_remote_function( - "plcupload", Binary(gzip.compress(fh.read())), send_name, - default_value=False - ) - except Exception as e: - pi.logger.error(e) - ec = -2 - break - - if not upload_status: - ec = -1 - break - - if ec == 0: + if uploader.ec == 0: # Tell user, we did not find the auto start program in files - if uploaded: + if not uploader.plc_program_included: QtWidgets.QMessageBox.information( self, self.tr("Information"), self.tr( "A PLC program has been uploaded. Please check the " @@ -115,7 +139,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files): ) ) - elif ec == -1: + elif uploader.ec == -1: QtWidgets.QMessageBox.critical( self, self.tr("Error"), self.tr( "The Revolution Pi could not process some parts of the " @@ -123,7 +147,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files): ) ) - elif ec == -2: + elif uploader.ec == -2: QtWidgets.QMessageBox.critical( self, self.tr("Error"), self.tr("Errors occurred during transmission") @@ -489,16 +513,16 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files): else: file_name = os.path.join(helper.cm.develop_watch_path, file_name) if override is None and os.path.exists(file_name): - rc = QtWidgets.QMessageBox.question( + rc_diag = QtWidgets.QMessageBox.question( self, self.tr("Override files..."), self.tr( "One or more files does exist on your computer! Do you want to override the existing" "files?\n\nSelect 'Yes' to override, 'No' to download only missing files." ), buttons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel ) - if rc == QtWidgets.QMessageBox.Cancel: + if rc_diag == QtWidgets.QMessageBox.Cancel: return - override = rc == QtWidgets.QMessageBox.Yes + override = rc_diag == QtWidgets.QMessageBox.Yes if os.path.exists(file_name) and not override: pi.logger.debug("Skip existing file '{0}'".format(file_name))