diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 6ea99c5..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/include/ui/simulator_ui.py b/include/ui/simulator_ui.py new file mode 100644 index 0000000..31e2bee --- /dev/null +++ b/include/ui/simulator_ui.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'simulator.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_simulator(object): + def setupUi(self, diag_simulator): + diag_simulator.setObjectName("diag_simulator") + diag_simulator.resize(522, 500) + self.verticalLayout = QtWidgets.QVBoxLayout(diag_simulator) + self.verticalLayout.setObjectName("verticalLayout") + self.gb_settings = QtWidgets.QGroupBox(diag_simulator) + self.gb_settings.setObjectName("gb_settings") + self.gridLayout = QtWidgets.QGridLayout(self.gb_settings) + self.gridLayout.setObjectName("gridLayout") + self.lbl_history = QtWidgets.QLabel(self.gb_settings) + self.lbl_history.setObjectName("lbl_history") + self.gridLayout.addWidget(self.lbl_history, 0, 0, 1, 1) + self.lbl_configrsc = QtWidgets.QLabel(self.gb_settings) + self.lbl_configrsc.setObjectName("lbl_configrsc") + self.gridLayout.addWidget(self.lbl_configrsc, 1, 0, 1, 1) + self.txt_configrsc = QtWidgets.QLineEdit(self.gb_settings) + self.txt_configrsc.setFocusPolicy(QtCore.Qt.NoFocus) + self.txt_configrsc.setText("") + self.txt_configrsc.setObjectName("txt_configrsc") + self.gridLayout.addWidget(self.txt_configrsc, 1, 1, 1, 1) + self.btn_configrsc = QtWidgets.QPushButton(self.gb_settings) + self.btn_configrsc.setObjectName("btn_configrsc") + self.gridLayout.addWidget(self.btn_configrsc, 1, 2, 1, 1) + self.lbl_procimg = QtWidgets.QLabel(self.gb_settings) + self.lbl_procimg.setObjectName("lbl_procimg") + self.gridLayout.addWidget(self.lbl_procimg, 2, 0, 1, 1) + self.txt_procimg = QtWidgets.QLineEdit(self.gb_settings) + self.txt_procimg.setFocusPolicy(QtCore.Qt.NoFocus) + self.txt_procimg.setText("") + self.txt_procimg.setObjectName("txt_procimg") + self.gridLayout.addWidget(self.txt_procimg, 2, 1, 1, 1) + self.lbl_stop = QtWidgets.QLabel(self.gb_settings) + self.lbl_stop.setObjectName("lbl_stop") + self.gridLayout.addWidget(self.lbl_stop, 3, 0, 1, 1) + self.lbl_restart = QtWidgets.QLabel(self.gb_settings) + self.lbl_restart.setObjectName("lbl_restart") + self.gridLayout.addWidget(self.lbl_restart, 4, 0, 1, 1) + self.cbb_history = QtWidgets.QComboBox(self.gb_settings) + self.cbb_history.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLength) + self.cbb_history.setObjectName("cbb_history") + self.gridLayout.addWidget(self.cbb_history, 0, 1, 1, 2) + self.cbx_stop_remove = QtWidgets.QCheckBox(self.gb_settings) + self.cbx_stop_remove.setObjectName("cbx_stop_remove") + self.gridLayout.addWidget(self.cbx_stop_remove, 3, 1, 1, 2) + self.rb_restart_pictory = QtWidgets.QRadioButton(self.gb_settings) + self.rb_restart_pictory.setChecked(True) + self.rb_restart_pictory.setObjectName("rb_restart_pictory") + self.gridLayout.addWidget(self.rb_restart_pictory, 4, 1, 1, 2) + self.rb_restart_zero = QtWidgets.QRadioButton(self.gb_settings) + self.rb_restart_zero.setObjectName("rb_restart_zero") + self.gridLayout.addWidget(self.rb_restart_zero, 5, 1, 1, 2) + self.verticalLayout.addWidget(self.gb_settings) + self.gb_info = QtWidgets.QGroupBox(diag_simulator) + self.gb_info.setObjectName("gb_info") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.gb_info) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.lbl_info = QtWidgets.QLabel(self.gb_info) + self.lbl_info.setWordWrap(True) + self.lbl_info.setObjectName("lbl_info") + self.verticalLayout_2.addWidget(self.lbl_info) + self.txt_info = QtWidgets.QPlainTextEdit(self.gb_info) + self.txt_info.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.txt_info.setReadOnly(True) + self.txt_info.setPlainText("") + self.txt_info.setObjectName("txt_info") + self.verticalLayout_2.addWidget(self.txt_info) + self.verticalLayout.addWidget(self.gb_info) + self.btn_start_pictory = QtWidgets.QPushButton(diag_simulator) + self.btn_start_pictory.setObjectName("btn_start_pictory") + self.verticalLayout.addWidget(self.btn_start_pictory) + self.btn_start_empty = QtWidgets.QPushButton(diag_simulator) + self.btn_start_empty.setObjectName("btn_start_empty") + self.verticalLayout.addWidget(self.btn_start_empty) + self.btn_start_nochange = QtWidgets.QPushButton(diag_simulator) + self.btn_start_nochange.setObjectName("btn_start_nochange") + self.verticalLayout.addWidget(self.btn_start_nochange) + + self.retranslateUi(diag_simulator) + self.btn_start_empty.clicked.connect(diag_simulator.accept) + self.btn_start_nochange.clicked.connect(diag_simulator.accept) + self.btn_start_pictory.clicked.connect(diag_simulator.accept) + QtCore.QMetaObject.connectSlotsByName(diag_simulator) + diag_simulator.setTabOrder(self.cbb_history, self.btn_configrsc) + diag_simulator.setTabOrder(self.btn_configrsc, self.cbx_stop_remove) + diag_simulator.setTabOrder(self.cbx_stop_remove, self.rb_restart_pictory) + diag_simulator.setTabOrder(self.rb_restart_pictory, self.rb_restart_zero) + diag_simulator.setTabOrder(self.rb_restart_zero, self.txt_info) + diag_simulator.setTabOrder(self.txt_info, self.btn_start_pictory) + diag_simulator.setTabOrder(self.btn_start_pictory, self.btn_start_empty) + diag_simulator.setTabOrder(self.btn_start_empty, self.btn_start_nochange) + + def retranslateUi(self, diag_simulator): + _translate = QtCore.QCoreApplication.translate + diag_simulator.setWindowTitle(_translate("diag_simulator", "piControl simulator")) + self.gb_settings.setTitle(_translate("diag_simulator", "Simulator settings")) + self.lbl_history.setText(_translate("diag_simulator", "Last used:")) + self.lbl_configrsc.setText(_translate("diag_simulator", "piCtory file:")) + self.btn_configrsc.setText(_translate("diag_simulator", "change...")) + self.lbl_procimg.setText(_translate("diag_simulator", "Process image:")) + self.lbl_stop.setText(_translate("diag_simulator", "Stop action:")) + self.lbl_restart.setText(_translate("diag_simulator", "Restart action:")) + self.cbx_stop_remove.setText(_translate("diag_simulator", "Remove process image")) + self.rb_restart_pictory.setText(_translate("diag_simulator", "Restore piCtory default values")) + self.rb_restart_zero.setText(_translate("diag_simulator", "Reset everything to ZERO")) + self.gb_info.setTitle(_translate("diag_simulator", "RevPiModIO integration")) + self.lbl_info.setText(_translate("diag_simulator", "You can work with this simulator if your call RevPiModIO with this additional parameters:")) + self.btn_start_pictory.setText(_translate("diag_simulator", "Start with piCtory default values")) + self.btn_start_pictory.setShortcut(_translate("diag_simulator", "Ctrl+1")) + self.btn_start_empty.setText(_translate("diag_simulator", "Start with empty process image")) + self.btn_start_empty.setShortcut(_translate("diag_simulator", "Ctrl+2")) + self.btn_start_nochange.setText(_translate("diag_simulator", "Start without changing actual process image")) + self.btn_start_nochange.setShortcut(_translate("diag_simulator", "Ctrl+3")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + diag_simulator = QtWidgets.QDialog() + ui = Ui_diag_simulator() + ui.setupUi(diag_simulator) + diag_simulator.show() + sys.exit(app.exec_()) diff --git a/include/ui_dev/simulator.ui b/include/ui_dev/simulator.ui new file mode 100644 index 0000000..d08cb7e --- /dev/null +++ b/include/ui_dev/simulator.ui @@ -0,0 +1,245 @@ + + + diag_simulator + + + + 0 + 0 + 522 + 500 + + + + piControl simulator + + + + + + Simulator settings + + + + + + Last used: + + + + + + + piCtory file: + + + + + + + Qt::NoFocus + + + + + + + + + + select... + + + + + + + Process image: + + + + + + + Qt::NoFocus + + + + + + + + + + Stop action: + + + + + + + Restart action: + + + + + + + QComboBox::AdjustToMinimumContentsLength + + + + + + + Remove process image + + + + + + + Restore piCtory default values + + + true + + + + + + + Reset everything to ZERO + + + + + + + + + + RevPiModIO integration + + + + + + You can work with this simulator if your call RevPiModIO with this additional parameters: + + + true + + + + + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + + + + + + + + + Start with piCtory default values + + + Ctrl+1 + + + + + + + Start with empty process image + + + Ctrl+2 + + + + + + + Start without changing actual process image + + + Ctrl+3 + + + + + + + cbb_history + btn_configrsc + cbx_stop_remove + rb_restart_pictory + rb_restart_zero + txt_info + btn_start_pictory + btn_start_empty + btn_start_nochange + + + + + btn_start_empty + clicked() + diag_simulator + accept() + + + 268 + 447 + + + 268 + 249 + + + + + btn_start_nochange + clicked() + diag_simulator + accept() + + + 268 + 478 + + + 268 + 249 + + + + + btn_start_pictory + clicked() + diag_simulator + accept() + + + 268 + 416 + + + 268 + 249 + + + + + diff --git a/revpicommander/helper.py b/revpicommander/helper.py index 1f7faae..a18f771 100644 --- a/revpicommander/helper.py +++ b/revpicommander/helper.py @@ -9,6 +9,7 @@ import socket from enum import IntEnum from http.client import CannotSendRequest from os import environ, remove +from os.path import exists from queue import Queue from threading import Lock from xmlrpc.client import Binary, ServerProxy @@ -273,7 +274,8 @@ class ConnectionManager(QtCore.QThread): self._revpi.cleanup() self._revpi_output.cleanup() - remove(self._revpi.procimg) + if settings.value("simulator/stop_remove", False, bool): + remove(self._revpi.procimg) self._revpi = None self._revpi_output = None @@ -297,17 +299,18 @@ class ConnectionManager(QtCore.QThread): self.connection_disconnected.emit() - def pyload_simulate(self, configrsc: str, procimg: str): + def pyload_simulate(self, configrsc: str, procimg: str, clean_existing: bool): """Start the simulator for piControl on local computer.""" pi.logger.debug("ConnectionManager.start_simulate") - with open(procimg, "wb") as fh: - fh.write(b'\x00' * 4096) + if not exists(procimg) or clean_existing: + with open(procimg, "wb") as fh: + fh.write(b'\x00' * 4096) try: import revpimodio2 - # Prepare process image with default values + # Prepare process image with default values for outputs self._revpi_output = revpimodio2.RevPiModIO(configrsc=configrsc, procimg=procimg) self._revpi_output.setdefaultvalues() self._revpi_output.writeprocimg() @@ -334,9 +337,14 @@ class ConnectionManager(QtCore.QThread): def reset_simulator(self): """Reset all io to piCtory defaults.""" pi.logger.debug("ConnectionManager.reset_simulator") - self._revpi_output.writeprocimg() - self._revpi.setdefaultvalues() - self._revpi.writeprocimg() + if settings.value("simulator/restart_zero", False, bool): + with open(self._revpi.procimg, "wb") as fh: + fh.write(b'\x00' * 4096) + self._revpi.readprocimg() + else: + self._revpi_output.writeprocimg() + self._revpi.setdefaultvalues() + self._revpi.writeprocimg() def run(self): """Thread worker to check status of RevPiPyLoad.""" diff --git a/revpicommander/revpicommander.py b/revpicommander/revpicommander.py index 5940111..3b12dd3 100755 --- a/revpicommander/revpicommander.py +++ b/revpicommander/revpicommander.py @@ -5,7 +5,7 @@ __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2018 Sven Sager" __license__ = "GPLv3" -__version__ = "0.9.1f" +__version__ = "0.9.1g" import webbrowser from os.path import basename, dirname, join @@ -22,6 +22,7 @@ from revpiinfo import RevPiInfo from revpioption import RevPiOption from revpiplclist import RevPiPlcList from revpiprogram import RevPiProgram +from simulator import Simulator from ui.revpicommander_ui import Ui_win_revpicommander @@ -195,26 +196,15 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander): """Start the simulator function.""" helper.cm.pyload_disconnect() - diag_open = QtWidgets.QFileDialog( - self, self.tr("Select downloaded piCtory file..."), - helper.settings.value("simulator_pictory", ".", str), - self.tr("piCtory file (*.rsc);;All files (*.*)") - ) - diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen) - diag_open.setFileMode(QtWidgets.QFileDialog.ExistingFile) - diag_open.setDefaultSuffix("rsc") - - if diag_open.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_open.selectedFiles()) != 1: + diag = Simulator(self) + if diag.exec() != QtWidgets.QDialog.Accepted: + diag.deleteLater() return - configrsc_file = diag_open.selectedFiles()[0] - dir_name = dirname(configrsc_file) - procimg_file = join(dir_name, "{0}.img".format( - basename(configrsc_file).rsplit(".", maxsplit=1)[0] - )) - helper.settings.setValue("simulator_pictory", configrsc_file) + configrsc_file = helper.settings.value("simulator/configrsc", "", str) + procimg_file = helper.settings.value("simulator/procimg", "", str) - if helper.cm.pyload_simulate(configrsc_file, procimg_file): + if helper.cm.pyload_simulate(configrsc_file, procimg_file, diag.cbx_stop_remove.isChecked()): QtWidgets.QMessageBox.information( self, self.tr("Simulator started..."), self.tr( "The simulator is running!\n\nYou can work with this simulator if your call " @@ -233,10 +223,12 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander): QtWidgets.QMessageBox.critical( self, self.tr("Can not start..."), self.tr( "Can not start the simulator! Maybe the piCtory file is corrupt " - "or you can not write to the location '{0}'." - ).format(dir_name) + "or you have no write permissions for '{0}'." + ).format(procimg_file) ) + diag.deleteLater() + @QtCore.pyqtSlot() def on_act_logs_triggered(self): """Show log window.""" @@ -407,10 +399,10 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander): if helper.cm.simulating: rc = QtWidgets.QMessageBox.question( self, self.tr("Reset to piCtory defaults..."), self.tr( - "Do you want to reset your process image to piCtory default values?\n" + "Do you want to reset your process image to {0} values?\n" "You have to stop other RevPiModIO programs before doing that, " "because they could reset the outputs." - ) + ).format("zero" if helper.settings.value("simulator/restart_zero", False, bool) else "piCtory default") ) == QtWidgets.QMessageBox.Yes if rc: # Set piCtory default values in process image @@ -494,7 +486,7 @@ if __name__ == "__main__": win = RevPiCommander() win.show() - exit_code = app.exec_() + exit_code = app.exec() # Clean up workers helper.cm.requestInterruption() diff --git a/revpicommander/revpiplclist.py b/revpicommander/revpiplclist.py index f5c7d01..9a51d12 100644 --- a/revpicommander/revpiplclist.py +++ b/revpicommander/revpiplclist.py @@ -25,7 +25,6 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections): def __init__(self, parent=None): super(RevPiPlcList, self).__init__(parent) self.setupUi(self) - self.setFixedSize(self.size()) self.__default_name = self.tr("New connection") self.__default_port = 55123 diff --git a/revpicommander/simulator.py b/revpicommander/simulator.py new file mode 100644 index 0000000..f791da7 --- /dev/null +++ b/revpicommander/simulator.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +"""Simulator for piControl.""" +__author__ = "Sven Sager" +__copyright__ = "Copyright (C) 2021 Sven Sager" +__license__ = "GPLv3" + +from os import W_OK, access +from os.path import basename, dirname, exists, join + +from PyQt5 import QtCore, QtGui, QtWidgets + +import helper +from ui.simulator_ui import Ui_diag_simulator + + +class Simulator(QtWidgets.QDialog, Ui_diag_simulator): + """ + This is a configuration dialog for the simulator of piControl. The + selected values will be saved in QSettings section 'simulator' and can be + accessed by simulator starting classes. + """ + + def __init__(self, parent=None): + super(Simulator, self).__init__(parent) + self.setupUi(self) + self.clean_procimg = False + self.max_items = 5 + + self.cbb_history.addItem("", "") + lst_configrsc = helper.settings.value("simulator/history_configrsc", [], list) + lst_procimg = helper.settings.value("simulator/history_procimg", [], list) + for i in range(len(lst_configrsc)): + self.cbb_history.addItem(lst_configrsc[i], lst_procimg[i]) + + self.cbx_stop_remove.setChecked(helper.settings.value("simulator/stop_remove", False, bool)) + self.rb_restart_pictory.setChecked(helper.settings.value("simulator/restart_pictory", False, bool)) + self.rb_restart_zero.setChecked(helper.settings.value("simulator/restart_zero", False, bool)) + + self.btn_start_pictory.setEnabled(False) + self.btn_start_empty.setEnabled(False) + self.btn_start_nochange.setEnabled(False) + + self.txt_configrsc.textChanged.connect(self.on_txt_textChanged) + self.txt_procimg.textChanged.connect(self.on_txt_textChanged) + + def _save_gui(self) -> None: + helper.settings.setValue("simulator/stop_remove", self.cbx_stop_remove.isChecked()) + helper.settings.setValue("simulator/restart_pictory", self.rb_restart_pictory.isChecked()) + helper.settings.setValue("simulator/restart_zero", self.rb_restart_zero.isChecked()) + + def accept(self) -> None: + self.cbb_history.removeItem(0) + if self.cbb_history.findText(self.txt_configrsc.text()) == -1: + self.cbb_history.addItem(self.txt_configrsc.text(), self.txt_procimg.text()) + if self.cbb_history.count() > self.max_items: + self.cbb_history.removeItem(self.max_items) + + helper.settings.setValue("simulator/configrsc", self.txt_configrsc.text()) + helper.settings.setValue("simulator/procimg", self.txt_procimg.text()) + self._save_gui() + + lst_configrsc = [] + lst_procimg = [] + for i in range(self.cbb_history.count()): + lst_configrsc.append(self.cbb_history.itemText(i)) + lst_procimg.append(self.cbb_history.itemData(i)) + helper.settings.setValue("simulator/history_configrsc", lst_configrsc) + helper.settings.setValue("simulator/history_procimg", lst_procimg) + + self.clean_procimg = self.sender() is self.btn_start_empty + + super(Simulator, self).accept() + + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + self._save_gui() + + @QtCore.pyqtSlot() + def on_btn_configrsc_clicked(self) -> None: + diag_open = QtWidgets.QFileDialog( + self, self.tr("Select downloaded piCtory file..."), + helper.settings.value("simulator/last_dir", ".", str), + self.tr("piCtory file (*.rsc);;All files (*.*)") + ) + diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen) + diag_open.setFileMode(QtWidgets.QFileDialog.ExistingFile) + diag_open.setDefaultSuffix("rsc") + + if diag_open.exec() != QtWidgets.QFileDialog.AcceptSave or len(diag_open.selectedFiles()) != 1: + diag_open.deleteLater() + return + + configrsc_file = diag_open.selectedFiles()[0] + dir_name = dirname(configrsc_file) + procimg_file = join(dir_name, "{0}.img".format(basename(configrsc_file).rsplit(".", maxsplit=1)[0])) + self.txt_configrsc.setText(configrsc_file) + self.txt_procimg.setText(procimg_file) + + helper.settings.setValue("simulator/last_dir", dir_name) + diag_open.deleteLater() + + @QtCore.pyqtSlot(int) + def on_cbb_history_currentIndexChanged(self, index: int) -> None: + if index == 0: + return + self.txt_configrsc.setText(self.cbb_history.itemText(index)) + self.txt_procimg.setText(self.cbb_history.itemData(index)) + + @QtCore.pyqtSlot(str) + def on_txt_textChanged(self, text: str) -> None: + configrsc_file = self.txt_configrsc.text() + procimg_file = self.txt_procimg.text() + if configrsc_file and procimg_file: + file_access = access(procimg_file, W_OK) + self.txt_info.setPlainText("configrsc=\"{0}\", procimg=\"{1}\"".format(configrsc_file, procimg_file)) + self.btn_start_pictory.setEnabled(file_access) + self.btn_start_empty.setEnabled(file_access) + self.btn_start_nochange.setEnabled(file_access and exists(procimg_file)) + else: + self.txt_info.clear() + self.btn_start_pictory.setEnabled(False) + self.btn_start_empty.setEnabled(False) + self.btn_start_nochange.setEnabled(False) diff --git a/setup.py b/setup.py index f6e7a51..e6ddb1d 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ class MyEggInfo(distutils.command.install_egg_info.install_egg_info): setup( - version="0.9.1f", + version="0.9.1g", python_requires="~=3.4", requires=["PyQt5", "revpimodio2", "zeroconf"],