47 Commits

Author SHA1 Message Date
b36e27a7f2 Clean up keyring when a connection is deleted 2023-01-10 23:23:55 +01:00
5488e7cc81 Bugfix for new settings management
Do not create new internal_id, if the setting already have one.
Do not create new entries, if the setting is the last one in the array.
2023-01-10 15:17:29 +01:00
8790e508ad Show just the hostname without .local in avahisearch.py list 2023-01-10 15:06:46 +01:00
7a7741e60b Save SSH credentials with keyring module
The keyring module will save the username and password of the SSH
connection to the operating system password storage.
2023-01-10 09:08:33 +01:00
2039f6cfe2 Set the use of ssh tunnel to default value for new settings 2023-01-09 20:47:35 +01:00
1e8200cc55 Change most button pressed event to clicked and add global shortcuts
Most events of the buttons was on pressed, which should not be used. Now
the debugcontrol buttons has a repeating pressed event, if you hold the
button, it will read the IOs every 200 ms. The shortcuts F4, F5, F6 to
read/write IOs are now application wide.
2023-01-09 19:40:00 +01:00
98b4879b7f Manage revolution pi saved settings in an own class
Start a complete new setting file with right naming. Import all old
saved connections from old settings file. Revolution Pi saved settings
are now in an own class with all properties and default values. All
modules are using this new setting class, which support the save
function.
2023-01-09 17:22:47 +01:00
bfe30513a7 Update Windows setup script to match pyinstaller output 2023-01-07 14:24:10 +01:00
09c95e2319 Update translation file 2023-01-07 13:57:45 +01:00
2c77d7e513 Update Makefile rules 2023-01-07 11:15:39 +01:00
d1a0457981 Choose connection type (ssh or xml-rpc) in avahisearch dialog 2023-01-07 11:12:58 +01:00
0c7192e1d4 Configure and start ssh tunneled connections 2023-01-07 10:36:53 +01:00
2f595f66aa Prepare SSH tunnel server and password GUI 2023-01-06 17:59:24 +01:00
da5d0a4a59 Use cm.call_remote_function to fetch log files via XML-RPC 2023-01-06 17:05:12 +01:00
dee2da0d8c QTranslator uses ui_languages to find translation
Loading translations was very static, now we use the array, the user
set in the gui to find a translation file.
2023-01-06 13:56:33 +01:00
d0e6eefd0e Add PyInstaller script for Windows 2023-01-05 15:55:30 +01:00
7f9111b5b0 Update Makefile and requirements.txt for build and clean ui files 2023-01-04 22:26:47 +01:00
2ac1477441 Add icons to commander main window for start, stop and so on 2023-01-04 21:43:20 +01:00
a557759063 Switch to setuptools for setup.py 2023-01-04 19:59:47 +01:00
3f2f3e0478 Switch project so src layout
This will make it possible to build pip packages.
Add a make file to compile UI files and build mac and win applications.
2023-01-04 18:12:59 +01:00
2400bd6951 Add context menu with copy and piCtory actions to revpi search dialog 2023-01-03 13:45:32 +01:00
9ec1f1b622 Bugfix: could not open debugcontrol, because of new layout names 2023-01-03 13:24:13 +01:00
0eb28f442e Add translation of status codes 2023-01-03 11:33:13 +01:00
375df9eaba Redesign main window with text boxes to copy host name or ip 2023-01-03 11:08:32 +01:00
0a7eb43d94 Implement update function of ZeroConf ServiceListener 2023-01-03 11:07:11 +01:00
03567dfefe Improved shortcuts to meet the needs of different operating systems 2022-12-01 10:06:04 +01:00
cef990df26 Fix qt parents of menu entries in connections 2022-12-01 09:57:04 +01:00
42f43726f6 Add Windows files for pyinstaller and setup generation 2022-11-01 14:52:58 +01:00
0164498a87 Design things: enable HighDpi, dependency versions, icon text 2021-11-19 10:33:13 +01:00
ea9a13e18c Bugfix: RevPi simulator could not be startet without existing proc.img file
The simulator will create a new proc.img file on startup if it does not exist.
2021-06-27 11:45:01 +02:00
0831e8253a Fix error in debugios.py with long byte values
Long byte values > 4 Bytes are handled as TEXT values. In some cases this could crash the sps monitor. Now, if we are not able to convert the bytes to a str(), we'll switch that io to "number" format.
2021-06-14 19:51:14 +02:00
c1e082107d New release 2021-06-13 21:32:47 +02:00
f215b3bbc1 Improved RevPi search window
Show names of already saved connections in parentheses
Show connection error message if we can not connect to RevPi
Save and restore geometry and column width
2021-06-13 21:30:59 +02:00
1354568eb6 Reduce max block to load for log files on slow networks 2021-06-13 20:35:35 +02:00
6bf453790a Fix crash during add new connection 2021-06-04 15:41:43 +02:00
61d543e6c7 Fix error on decoding broken log files 2021-06-03 21:14:18 +02:00
4eec6306a9 Add upload progress display in developer, bugfix on file download in developer 2021-04-26 12:07:47 +02:00
8a2cd9f304 New release 2021-04-25 13:27:42 +02:00
6be0c8d32f Modified translations 2021-04-25 13:17:10 +02:00
3ea43e2931 Bugfix for simulator to prevent GUI crash after first use 2021-04-25 12:44:40 +02:00
cda60e119c Add a simulator dialog to manage simulator settings 2021-04-25 11:55:19 +02:00
f0e6f64389 Add parameter to change connection timeout (default was and is 5 seconds) 2021-04-18 11:03:51 +02:00
2341e2be95 Bugfix for translations strings and format function 2021-04-15 16:09:03 +02:00
9f8409f375 Get AVAHI IPv4 addresses with service_info 2021-04-10 15:42:04 +02:00
bf1c5e6e5d New release 2021-02-17 07:26:56 +01:00
989ba6ab14 Remove included revpimodio2 static link and add it as requires 2021-02-17 07:25:44 +01:00
4fa5767685 Bugfix: Load locales file 2021-01-16 12:30:11 +01:00
93 changed files with 7896 additions and 3843 deletions

4
.idea/misc.xml generated
View File

@@ -4,9 +4,9 @@
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectPlainTextFileTypeManager">
<file url="file://$PROJECT_DIR$/revpicommander/locale/revpicommander_de.ts" />
<file url="file://$PROJECT_DIR$/src/revpicommander/locale/revpicommander_de.ts" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (revpicommander)" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>

View File

@@ -2,11 +2,14 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/include" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/revpicommander" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/lib/revpimodio2" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.9 (revpicommander)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings">
<option name="versionSpecifier" value="Greater or equal (&gt;=x.y.z)" />
</component>
</module>

8
.idea/vcs.xml generated
View File

@@ -1,7 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="BodyLimit" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SubjectBodySeparation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SubjectLimit" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/lib/revpimodio2" vcs="Git" />
</component>
</project>

View File

@@ -1,8 +1,12 @@
include MANIFEST.in
include stdeb.cfg
recursive-include data *
recursive-include include *.py
recursive-include lib *.py
recursive-include revpicommander *.py *.qm
global-exclude *.pyc
recursive-include src/revpicommander *.py *.qm
recursive-include ui_dev *
include LICENSE.txt
include make_installer_win.bat
include Makefile
include MANIFEST.in
include README.md
include requirements.txt
include setup.iss
include setup.py
include translate.pro

83
Makefile Normal file
View File

@@ -0,0 +1,83 @@
SHELL := bash
MAKEFLAGS = --no-print-directory --no-builtin-rules
.DEFAULT_GOAL = all
# Variables
PACKAGE = revpicommander
# If virtualenv exists, use it. If not, use PATH to find
SYSTEM_PYTHON = $(or $(shell which python3), $(shell which python))
PYTHON = $(or $(wildcard venv/bin/python), $(SYSTEM_PYTHON))
SYSTEM_PYUIC5 = $(shell which pyuic5)
PYUIC5 = $(or $(wildcard venv/bin/pyuic5), $(SYSTEM_PYUIC5))
SYSTEM_PYRCC5 = $(shell which pyrcc5)
PYRCC5 = $(or $(wildcard venv/bin/pyrcc5), $(SYSTEM_PYRCC5))
SYSTEM_PYLUP5 = $(shell which pylupdate5)
PYLUP5 = $(or $(wildcard venv/bin/pylupdate5), $(SYSTEM_PYLUP5))
all: build_ui build_rc build
.PHONY: all
## Compile Qt UI files to python code
build_ui:
cd ui_dev && for ui_file in *.ui; do \
file_name=$${ui_file%.ui}; \
$(PYUIC5) $${ui_file} -o ../src/$(PACKAGE)/ui/$${file_name}_ui.py -x --from-imports; \
echo $${file_name}; \
done
build_rc:
cd ui_dev && for rc_file in *.qrc; do \
file_name=$${rc_file%.qrc}; \
$(PYRCC5) $${rc_file} -o ../src/$(PACKAGE)/ui/$${file_name}_rc.py; \
echo $${file_name}; \
done
update_translation:
$(PYLUP5) translate.pro
.PHONY: build_ui build_rc update_translation
## Environment
venv:
rm -rf venv
$(SYSTEM_PYTHON) -m venv venv
deps:
$(PYTHON) -m pip install --upgrade pip -r requirements.txt
.PHONY: venv deps
## Build, install
build: build_ui build_rc
$(PYTHON) -m setup sdist
$(PYTHON) -m setup bdist_wheel
install:
$(PYTHON) -m pip install dist/$(PACKAGE)-*.whl
.PHONY: build install
## PyInstaller
installer_mac: build
$(PYTHON) -m PyInstaller -n "RevPi Commander" \
--add-data="src/$(PACKAGE)/locale:./revpicommander/locale" \
--add-data="data/$(PACKAGE).icns:." \
--icon=data/$(PACKAGE).icns \
--noconfirm \
--clean \
--onedir \
--windowed \
src/$(PACKAGE)/__main__.py
installer_win: all
make_installer_win.bat
.PHONY: installer_mac installer_win
## Clean
clean:
rm -rf build dist src/*.egg-info *.spec
.PHONY: clean

View File

@@ -1,7 +1,6 @@
[Desktop Entry]
Name=RevPi PLC Commander
Comment=Controls the Python PLC program on your Revolution PI
Name[de]=RevPi PLC Steuerung
Comment[de]=Kontrolliert das Python PLC Programm auf dem Revolution PI
Exec=/usr/bin/revpicommander
Icon=revpicommander

BIN
data/revpicommander.icns Normal file

Binary file not shown.

View File

@@ -1,218 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>diag_connections</class>
<widget class="QDialog" name="diag_connections">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>520</width>
<height>508</height>
</rect>
</property>
<property name="windowTitle">
<string>Revolution Pi connections</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeWidget" name="tre_connections">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<column>
<property name="text">
<string>Connection name</string>
</property>
</column>
<column>
<property name="text">
<string>Address</string>
</property>
</column>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="vl_edit">
<item>
<spacer name="vs_edit">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btn_up">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/arrow-up.ico</normaloff>:/action/ico/arrow-up.ico</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_down">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/arrow-down.ico</normaloff>:/action/ico/arrow-down.ico</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_delete">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/edit-delete.ico</normaloff>:/action/ico/edit-delete.ico</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_add">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/edit-add.ico</normaloff>:/action/ico/edit-add.ico</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="gb_properties">
<property name="title">
<string>Connection properties</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="lbl_name">
<property name="text">
<string>Display name:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lbl_folder">
<property name="text">
<string>Sub folder:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_address">
<property name="text">
<string>Address (DNS/IP):</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lbl_port">
<property name="text">
<string>Port (Default {0}):</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txt_name"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="txt_address"/>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="sbx_port">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>55123</number>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="cbb_folder">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string/>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="btn_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Discard|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="ressources.qrc"/>
</resources>
<connections>
<connection>
<sender>btn_box</sender>
<signal>accepted()</signal>
<receiver>diag_connections</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>btn_box</sender>
<signal>rejected()</signal>
<receiver>diag_connections</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

18
make_installer_win.bat Normal file
View File

@@ -0,0 +1,18 @@
@echo off
set PACKAGE=revpicommander
rem python -m venv venv
rem venv\\Scripts\\activate.bat
rem pip install -r requirements.txt
pyinstaller -n "RevPi Commander" ^
--add-data="src\%PACKAGE%\locale;.\revpicommander\locale" ^
--add-data="data\%PACKAGE%.ico;." ^
--icon=data\\%PACKAGE%.ico ^
--noconfirm ^
--clean ^
--onedir ^
--windowed ^
src\\%PACKAGE%\\__main__.py
rem deactivate

8
requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
Pyinstaller
PyQt5>=5.14.1
revpimodio2>=2.5.6
zeroconf>=0.24.4
setuptools>=65.6.3
wheel
paramiko>=2.12.0
keyring>=23.13.1

View File

@@ -1,239 +0,0 @@
# -*- coding: utf-8 -*-
"""Revolution Pi search with zeroconf."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2020 Sven Sager"
__license__ = "GPLv3"
from os import name as osname
from re import compile
from socket import gethostbyname
from PyQt5 import QtCore, QtGui, QtWidgets
from zeroconf import ServiceBrowser, Zeroconf
import helper
import proginit as pi
from helper import WidgetData
from ui.avahisearch_ui import Ui_diag_search
class AvahiSearchThread(QtCore.QThread):
"""Search thread for Revolution Pi with installed RevPiPyLoad."""
added = QtCore.pyqtSignal(str, str, int, str, str)
removed = QtCore.pyqtSignal(str, str)
def __init__(self, parent=None):
super(AvahiSearchThread, self).__init__(parent)
self._cycle_wait_ms = 1000
self.__dict_arp = {}
self.re_posix = compile(
r"(?P<ip>(\d{1,3}\.){3}\d{1,3}).*"
r"(?P<mac>([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
)
def _update_arp(self):
"""Find mac address in arp table."""
if osname == "posix":
with open("/proc/net/arp") as fh:
for line in fh.readlines():
ip_mac = self.re_posix.search(line)
if ip_mac:
self.__dict_arp[ip_mac.group("ip")] = ip_mac.group("mac")
def get_mac(self, ip: str):
"""
Get mac address of ip, if known.
:param ip: IP address to find mac address
:return: MAC address as string or empty string, if unknown
"""
return self.__dict_arp.get(ip, "")
def remove_service(self, zeroconf: Zeroconf, conf_type: str, name: str):
"""Revolution Pi disappeared."""
pi.logger.debug("AvahiSearchThread.remove_service")
self.removed.emit(name, conf_type)
def add_service(self, zeroconf: Zeroconf, conf_type: str, name: str):
"""New Revolution Pi found."""
pi.logger.debug("AvahiSearchThread.add_service")
info = zeroconf.get_service_info(conf_type, name)
if not info:
return
try:
ip = gethostbyname(info.server)
except Exception:
ip = self.tr("N/A")
self.added.emit(name, info.server, info.port, conf_type, ip)
def run(self):
pi.logger.debug("Started zero conf discovery.")
zeroconf = Zeroconf()
revpi_browser = ServiceBrowser(zeroconf, "_revpipyload._tcp.local.", self)
while not self.isInterruptionRequested():
# Just hanging around :)
self.msleep(self._cycle_wait_ms)
zeroconf.close()
pi.logger.debug("Stopped zero conf discovery.")
class AvahiSearch(QtWidgets.QDialog, Ui_diag_search):
def __init__(self, parent=None):
super(AvahiSearch, self).__init__(parent)
self.setupUi(self)
self.setFixedSize(self.size())
self.connect_index = -1
self.th_zero_conf = AvahiSearchThread(self)
self.tb_revpi.setColumnWidth(0, 250)
self.btn_connect.setEnabled(False)
self.btn_save.setEnabled(False)
def _restart_search(self):
"""Clean up an restart search thread."""
while self.tb_revpi.rowCount() > 0:
self.tb_revpi.removeRow(0)
self.th_zero_conf.requestInterruption()
self.th_zero_conf = AvahiSearchThread(self)
self.th_zero_conf.added.connect(self.on_avahi_added)
self.th_zero_conf.removed.connect(self.on_avahi_removed)
self.th_zero_conf.start()
def _save_connection(self, row: int, no_warn=False):
"""
Save the connection from given row to settings.
:param row: Row with connection data
:param no_warn: If True, no message boxes will appear
:return: Array index of connection (found or saved)
"""
item = self.tb_revpi.item(row, 0)
if not item:
return
folder_name = self.tr("Auto discovered")
selected_name = item.text()
selected_address = item.data(WidgetData.address)
selected_port = item.data(WidgetData.port)
i = 0
for i in range(helper.settings.beginReadArray("connections")):
helper.settings.setArrayIndex(i)
name = helper.settings.value("name", type=str)
address = helper.settings.value("address", type=str)
port = helper.settings.value("port", type=int)
if address.lower() == selected_address.lower() and port == selected_port:
if not no_warn:
QtWidgets.QMessageBox.information(
self, self.tr("Already in list..."), self.tr(
"The selected Revolution Pi is already saved in your "
"connection list as '{0}.".format(name)
)
)
helper.settings.endArray()
return i
helper.settings.endArray()
helper.settings.beginWriteArray("connections")
helper.settings.setArrayIndex(i + 1)
helper.settings.setValue("address", selected_address)
helper.settings.setValue("folder", folder_name)
helper.settings.setValue("name", selected_name)
helper.settings.setValue("port", selected_port)
helper.settings.endArray()
if not no_warn:
QtWidgets.QMessageBox.information(
self, self.tr("Success"), self.tr(
"The connection with the name '{0}' was successfully saved "
"to folder '{1}' in your connections.".format(selected_name, folder_name)
)
)
return i + 1
def exec(self) -> int:
self._restart_search()
rc = super(AvahiSearch, self).exec()
self.th_zero_conf.requestInterruption()
return rc
@QtCore.pyqtSlot(str, str, int, str, str)
def on_avahi_added(self, name: str, server: str, port: int, conf_type: str, ip: str):
"""New Revolution Pi found."""
index = -1
for i in range(self.tb_revpi.rowCount()):
if self.tb_revpi.item(i, 0).data(WidgetData.object_name) == name:
index = i
break
if index == -1:
# New Row
item_name = QtWidgets.QTableWidgetItem()
item_ip = QtWidgets.QTableWidgetItem()
index = self.tb_revpi.rowCount()
self.tb_revpi.insertRow(index)
self.tb_revpi.setItem(index, 0, item_name)
self.tb_revpi.setItem(index, 1, item_ip)
else:
# Update row
item_name = self.tb_revpi.item(index, 0)
item_ip = self.tb_revpi.item(index, 1)
item_name.setIcon(QtGui.QIcon(":/main/ico/cpu.ico"))
item_name.setText(server[:-1])
item_name.setData(WidgetData.object_name, name)
item_name.setData(WidgetData.address, ip)
item_name.setData(WidgetData.port, port)
item_ip.setText(ip)
@QtCore.pyqtSlot(str, str)
def on_avahi_removed(self, name: str, conf_type: str):
"""Revolution Pi disappeared."""
for i in range(self.tb_revpi.rowCount()):
if self.tb_revpi.item(i, 0).data(WidgetData.object_name) == name:
self.tb_revpi.removeRow(i)
break
@QtCore.pyqtSlot(int, int)
def on_tb_revpi_cellDoubleClicked(self, row: int, column: int):
"""Connect to double clicked Revolution Pi."""
pi.logger.debug("AvahiSearch.on_tb_revpi_cellDoubleClicked")
self.connect_index = self._save_connection(row, no_warn=True)
self.accept()
@QtCore.pyqtSlot(int, int, int, int)
def on_tb_revpi_currentCellChanged(self, row: int, column: int, last_row: int, last_column: int):
"""Manage state of buttons."""
self.btn_connect.setEnabled(row >= 0)
self.btn_save.setEnabled(row >= 0)
@QtCore.pyqtSlot()
def on_btn_connect_pressed(self):
"""Connect to selected Revolution Pi."""
pi.logger.debug("AvahiSearch.on_btn_connect_pressed")
if self.tb_revpi.currentRow() == -1:
return
self.connect_index = self._save_connection(self.tb_revpi.currentRow(), no_warn=True)
self.accept()
@QtCore.pyqtSlot()
def on_btn_save_pressed(self):
"""Save selected Revolution Pi."""
pi.logger.debug("AvahiSearch.on_btn_save_pressed")
if self.tb_revpi.currentRow() == -1:
return
self.connect_index = self._save_connection(self.tb_revpi.currentRow())
@QtCore.pyqtSlot()
def on_btn_restart_pressed(self):
"""Clean up an restart search thread."""
self._restart_search()

View File

@@ -1,463 +0,0 @@
# -*- coding: utf-8 -*-
"""Helper functions for this application."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2020 Sven Sager"
__license__ = "GPLv3"
import pickle
import socket
from enum import IntEnum
from http.client import CannotSendRequest
from os import environ, remove
from queue import Queue
from threading import Lock
from xmlrpc.client import Binary, ServerProxy
from PyQt5 import QtCore
import proginit as pi
class WidgetData(IntEnum):
address = 260
replace_ios_config = 261
acl_level = 262
has_error = 263
port = 264
object_name = 265
last_dir_upload = 301
last_file_upload = 302
last_dir_pictory = 303
last_dir_picontrol = 304
last_dir_selected = 305
last_pictory_file = 306
last_tar_file = 307
last_zip_file = 308
file_name = 309
watch_files = 310
watch_path = 311
debug_geos = 312
class ConnectionManager(QtCore.QThread):
"""Check connection and status for PLC program on Revolution Pi."""
connection_established = QtCore.pyqtSignal()
"""New connection established successfully with <class 'ServerProxy'>."""
connection_disconnected = QtCore.pyqtSignal()
"""Connection disconnected."""
connection_disconnecting = QtCore.pyqtSignal()
"""Signal emitted before closing connection."""
connection_error_observed = QtCore.pyqtSignal(str)
"""This will be triggered, if a connection error was detected."""
status_changed = QtCore.pyqtSignal(str, str)
"""Status message and color suggestion."""
def __init__(self, parent=None, cycle_time_ms=1000):
super(ConnectionManager, self).__init__(parent)
self._cli = None
self._cli_connect = Queue()
self._cycle_time = cycle_time_ms
self._lck_cli = Lock()
self._ps_started = False
self._revpi = None
self._revpi_output = None
self.address = ""
self.name = ""
self.port = 55123
# Sync this with revpiplclist to preserve settings
self.program_last_dir_upload = ""
self.program_last_file_upload = ""
self.program_last_dir_pictory = ""
self.program_last_dir_picontrol = ""
self.program_last_dir_selected = ""
self.program_last_pictory_file = ""
self.program_last_tar_file = ""
self.program_last_zip_file = ""
self.develop_watch_files = []
self.develop_watch_path = ""
self.debug_geos = {}
self.pyload_version = (0, 0, 0)
"""Version number of RevPiPyLoad 0.0.0 with <class 'int'> values."""
self.xml_funcs = []
"""Name list of all supported functions of RevPiPyLoad."""
self.xml_mode = -1
"""ACL level for this connection (-1 on connection denied)."""
self._xml_mode_refresh = False
def __call_simulator(self, function_name: str, *args, default_value=None, **kwargs):
pi.logger.debug("ConnectionManager.__call_simulator({0})".format(function_name))
if function_name == "ps_values":
if self._revpi.readprocimg():
bytebuff = bytearray()
for dev in self._revpi.device:
bytebuff += bytes(dev)
return Binary(bytes(bytebuff))
elif function_name == "ps_setvalue":
# args: 0=device, 1=io, 2=value
device = args[0]
io = args[1]
if type(args[2]) == Binary:
value = args[2].data
else:
value = args[2]
try:
# Write new value to IO
self._revpi.io[io].set_value(value)
except Exception as e:
return [device, io, False, str(e)]
return [device, io, True, ""]
elif function_name == "psstart":
self._revpi.autorefresh_all()
return True
elif function_name == "psstop":
self._revpi.exit(full=False)
return True
elif function_name == "ps_devices":
return [(dev.position, dev.name) for dev in self._revpi.device]
elif function_name == "ps_inps":
return self.__simulator_ios("inp")
elif function_name == "ps_outs":
return self.__simulator_ios("out")
else:
return default_value
def __simulator_ios(self, iotype: str):
dict_ios = {}
for dev in self._revpi.device:
dict_ios[dev.position] = []
if iotype == "inp":
lst_io = dev.get_inputs()
elif iotype == "out":
lst_io = dev.get_outputs()
else:
lst_io = []
for io in lst_io:
dict_ios[dev.position].append([
io.name,
1 if io._bitlength == 1 else int(io._bitlength / 8),
io._slc_address.start + dev.offset,
io.bmk,
io._bitaddress,
io._byteorder,
io._signed,
])
return Binary(pickle.dumps(dict_ios))
def _clear_settings(self):
"""Clear connection settings."""
self.address = ""
self.name = ""
self.port = 55123
self.pyload_version = (0, 0, 0)
self.xml_funcs.clear()
self.xml_mode = -1
self.program_last_dir_upload = ""
self.program_last_file_upload = ""
self.program_last_dir_pictory = ""
self.program_last_dir_picontrol = ""
self.program_last_dir_selected = ""
self.program_last_pictory_file = ""
self.program_last_tar_file = ""
self.program_last_zip_file = ""
self.develop_watch_files = []
self.develop_watch_path = ""
self.debug_geos = {}
def _save_settings(self):
"""Save settings to named Revolution Pi."""
for i in range(settings.beginReadArray("connections")):
settings.setArrayIndex(i)
if settings.value("address") != self.address:
# Search used connection, because connection manager could reorganize array
continue
settings.setValue("last_dir_upload", self.program_last_dir_upload)
settings.setValue("last_file_upload", self.program_last_file_upload)
settings.setValue("last_dir_pictory", self.program_last_dir_pictory)
settings.setValue("last_dir_picontrol", self.program_last_dir_picontrol)
settings.setValue("last_dir_selected", self.program_last_dir_selected)
settings.setValue("last_pictory_file", self.program_last_pictory_file)
settings.setValue("last_tar_file", self.program_last_tar_file)
settings.setValue("last_zip_file", self.program_last_zip_file)
settings.setValue("watch_files", self.develop_watch_files)
settings.setValue("watch_path", self.develop_watch_path)
settings.setValue("debug_geos", self.debug_geos)
break
settings.endArray()
def pyload_connect(self, settings_index: int):
"""
Create a new connection from settings object.
:param settings_index: Index of settings array 'connections'
:return: True, if the connection was successfully established
"""
# First disconnect to send signal and clean up values
self.pyload_disconnect()
settings.beginReadArray("connections")
settings.setArrayIndex(settings_index)
address = settings.value("address")
name = settings.value("name")
port = settings.value("port", defaultValue=55123)
self.program_last_dir_upload = settings.value("last_dir_upload", ".", str)
self.program_last_file_upload = settings.value("last_file_upload", ".", str)
self.program_last_dir_pictory = settings.value("last_dir_pictory", ".", str)
self.program_last_dir_picontrol = settings.value("last_dir_picontrol", ".", str)
self.program_last_dir_selected = settings.value("last_dir_selected", ".", str)
self.program_last_pictory_file = settings.value("last_pictory_file", "{0}.rsc".format(name), str)
self.program_last_tar_file = settings.value("last_tar_file", "{0}.tgz".format(name))
self.program_last_zip_file = settings.value("last_zip_file", "{0}.zip".format(name))
self.develop_watch_files = settings.value("watch_files", [], list)
self.develop_watch_path = settings.value("watch_path", "", str)
self.debug_geos = settings.value("debug_geos", {}, dict)
settings.endArray()
socket.setdefaulttimeout(2)
sp = ServerProxy("http://{0}:{1}".format(address, port))
# Load values and test connection to Revolution Pi
try:
pyload_version = tuple(map(int, sp.version().split(".")))
xml_funcs = sp.system.listMethods()
xml_mode = sp.xmlmodus()
except Exception as e:
self.connection_error_observed.emit(str(e))
return False
self.address = address
self.name = name
self.port = port
self.pyload_version = pyload_version
self.xml_funcs = xml_funcs
self.xml_mode = xml_mode
with self._lck_cli:
socket.setdefaulttimeout(5)
self._cli = sp
self._cli_connect.put_nowait((address, port))
self.connection_established.emit()
return True
def pyload_disconnect(self):
"""Disconnect from Revolution Pi."""
if self._revpi is not None:
self.connection_disconnecting.emit()
self._revpi.cleanup()
self._revpi_output.cleanup()
remove(self._revpi.procimg)
self._revpi = None
self._revpi_output = None
pi.logger.debug("Simulator destroyed.")
self.connection_disconnected.emit()
elif self._cli is not None:
# Tell all widget, that we want do disconnect, to save the settings
self.connection_disconnecting.emit()
self._save_settings()
with self._lck_cli:
if self._ps_started:
try:
self._cli.psstop()
except Exception:
pass
self._clear_settings()
self._cli = None
self.connection_disconnected.emit()
def pyload_simulate(self, configrsc: str, procimg: str):
"""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)
try:
import revpimodio2
# Prepare process image with default values
self._revpi_output = revpimodio2.RevPiModIO(configrsc=configrsc, procimg=procimg)
self._revpi_output.setdefaultvalues()
self._revpi_output.writeprocimg()
# This is our simulator to work with
self._revpi = revpimodio2.RevPiModIO(simulator=True, configrsc=configrsc, procimg=procimg)
self._revpi.setdefaultvalues()
self._revpi.writeprocimg()
self.xml_funcs = ["psstart", "psstop", "ps_devices", "ps_inps", "ps_outs", "ps_values", "ps_setvalue"]
except Exception as e:
pi.logger.exception(e)
self._revpi_output = None
self._revpi = None
remove(procimg)
return self._revpi is not None
def refresh_xml_mode(self):
"""Refresh XML ACL level after some change could be done."""
self._xml_mode_refresh = True
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()
def run(self):
"""Thread worker to check status of RevPiPyLoad."""
self.setPriority(QtCore.QThread.NormalPriority)
sp = None
while not self.isInterruptionRequested():
if self._revpi is not None:
sp = None
self.status_changed.emit("SIMULATING", "yellow")
elif self._cli is None:
sp = None
self.status_changed.emit("NOT CONNECTED", "lightblue")
elif not self._cli_connect.empty():
# Get new connection information to create object in this thread
item = self._cli_connect.get()
sp = ServerProxy("http://{0}:{1}".format(*item))
self._cli_connect.task_done()
if sp:
try:
plc_exit_code = sp.plcexitcode()
if self._xml_mode_refresh:
self.xml_mode = sp.xmlmodus()
self._xml_mode_refresh = False
except CannotSendRequest as e:
pi.logger.warning(e)
except Exception as e:
pi.logger.warning(e)
self.status_changed.emit("SERVER ERROR", "red")
self.connection_error_observed.emit("{0} | {1}".format(e, type(e)))
else:
if plc_exit_code == -1:
self.status_changed.emit("RUNNING", "green")
elif plc_exit_code == -2:
self.status_changed.emit("FILE NOT FOUND", "red")
elif plc_exit_code == -3:
self.status_changed.emit("NOT RUNNING (NO STATUS)", "yellow")
elif plc_exit_code == -9:
self.status_changed.emit("PROGRAM KILLED", "red")
elif plc_exit_code == -15:
self.status_changed.emit("PROGRAM TERMED", "red")
elif plc_exit_code == 0:
self.status_changed.emit("NOT RUNNING", "yellow")
else:
self.status_changed.emit("FINISHED WITH CODE {0}".format(plc_exit_code), "yellow")
self.msleep(self._cycle_time)
def call_remote_function(self, function_name: str, *args, default_value=None, raise_exception=False, **kwargs):
"""
Save call of a remote function with given name and parameters on Revolution Pi.
:param function_name: Function to call on RevPiPyLoad
:param args: Functions arguments
:param default_value: Default value will be returned on error
:param raise_exception: Will raise the exception returned from server
:param kwargs: Functions key word arguments
:return: Return value of remote function or default_value
"""
if self._cli is None and self._revpi is None:
pi.logger.error("Not connected while calling {0}".format(function_name))
if raise_exception:
raise ConnectionError("Connection manager not connected")
return default_value
reload_funcs = False
if function_name == "psstart":
self._ps_started = True
reload_funcs = True
elif function_name == "psstop":
self._ps_started = False
reload_funcs = True
# On connection problems do not freeze
if self._lck_cli.acquire(timeout=1.0):
if self._revpi is not None:
# Redirect call to simulator
return_value = self.__call_simulator(function_name, *args, default_value=default_value, **kwargs)
else:
try:
return_value = getattr(self._cli, function_name)(*args, **kwargs)
if reload_funcs:
self.xml_funcs = self._cli.system.listMethods()
except Exception as e:
pi.logger.error(e)
if raise_exception:
self._lck_cli.release()
raise
return_value = default_value
self._lck_cli.release()
return return_value
elif raise_exception:
raise ConnectionError("Can not get lock of connection")
return default_value
def get_cli(self):
"""Connection proxy of actual connection."""
if self.address and self.port:
return ServerProxy("http://{0}:{1}".format(self.address, self.port))
else:
return None
@property
def connected(self):
"""True if we have an active connection."""
return self._cli is not None
@property
def simulating(self):
"""True, if simulating mode is running."""
return self._revpi is not None
cm = ConnectionManager()
"""Clobal connection manager instance."""
settings = QtCore.QSettings("revpipyplc", "revpipyload")
"""Global application settings."""
homedir = environ.get("HOME", "") or environ.get("APPDATA", "")
"""Home dir of user."""

48
setup.iss Executable file
View File

@@ -0,0 +1,48 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "RevPi Commander"
#define MyAppVersion "0.9.10rc2"
#define MyAppPublisher "Sven Sager"
#define MyAppURL "https://revpimodio.org/"
#define MyAppICO "data\revpicommander.ico"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{21E8D429-0C18-462F-AFC0-56EA664DE629}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
LicenseFile=LICENSE.txt
; Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputDir=dist
OutputBaseFilename={#MyAppName} {#MyAppVersion}
SetupIconFile={#MyAppICO}
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "dist\{#MyAppName}\{#MyAppName}.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "dist\{#MyAppName}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppName}.exe"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppName}.exe"; Tasks: desktopicon

View File

@@ -1,57 +1,47 @@
# -*- coding: utf-8 -*-
"""Setupscript fuer RevPiPyLoad."""
"""Setupscript for RevPiCommander."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__license__ = "LGPLv3"
import distutils.command.install_egg_info
from distutils.core import setup
from glob import glob
class MyEggInfo(distutils.command.install_egg_info.install_egg_info):
u"""Disable egg_info installation, seems pointless for a non-library."""
def run(self):
u"""just pass egg_info."""
pass
__license__ = "GPLv3"
from setuptools import find_namespace_packages, setup
setup(
version="0.9.0",
python_requires="~=3.4",
requires=["PyQt5", "zeroconf"],
scripts=["data/revpicommander"],
data_files=[
("share/applications", ["data/revpicommander.desktop"]),
("share/icons/hicolor/32x32/apps", ["data/revpicommander.png"]),
("share/revpicommander", glob("revpicommander/*.py")),
("share/revpicommander/revpimodio2", glob("lib/revpimodio2/revpimodio2/*.py")),
("share/revpicommander/ui", glob("include/ui/*.py")),
("share/revpicommander/locale/", glob("revpicommander/locale/*.qm")),
],
# Additional meta-data
name="revpicommander",
version="0.9.10rc2",
packages=find_namespace_packages("src"),
package_dir={'': 'src'},
include_package_data=True,
install_requires=[
"keyring",
"PyQt5",
"revpimodio2",
"zeroconf"
],
entry_points={
'gui_scripts': [
'revpicommander-gui = revpicommander.revpicommander:main',
],
},
platforms=["all"],
url="https://revpimodio.org/revpipyplc/",
license="GPLv3",
author="Sven Sager",
author_email="akira@narux.de",
maintainer="Sven Sager",
maintainer_email="akira@revpimodio.org",
url="https://revpimodio.org/revpipyplc/",
description="GUI for Revolution Pi to upload programs and do IO-Checks",
long_description=""
"Dieses Programm startet beim Systemstart ein angegebenes Python PLC\n"
"Programm. Es überwacht das Programm und startet es im Fehlerfall neu.\n"
"Bei Abstruz kann das gesamte /dev/piControl0 auf 0x00 gesettz werden.\n"
"Außerdem stellt es einen XML-RPC Server bereit, über den die Software\n"
"auf den RevPi geladen werden kann. Das Prozessabbild kann über ein Tool\n"
"zur Laufzeit überwacht werden.",
long_description="The RevPiCommander is a GUI tool to manage your revolution Pi over the\n"
"network. You can search for RevPis in your network, manage the settings\n"
"of RevPiPyLoad and do IO checks on your local machine. Developing your\n"
"control program is very easy with the developer, upload and debug it\n"
"over the network.",
keywords=["revpi", "revolution pi", "revpimodio", "plc"],
classifiers=[
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: POSIX :: Linux",
],
license="GPLv3",
cmdclass={"install_egg_info": MyEggInfo},
)

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""Package: RevPiCommander."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""Start main application of this package."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
# If we are running from a wheel, add the wheel to sys.path
if __package__ == "":
from os.path import dirname
from sys import path
# __file__ is package-*.whl/package/__main__.py
# Resulting path is the name of the wheel itself
package_path = dirname(dirname(__file__))
path.insert(0, package_path)
if __name__ == "__main__":
import sys
from revpicommander.revpicommander import main
# Run the main application of this package
sys.exit(main())

View File

@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
"""Manager for ACL lists."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from re import compile
from PyQt5 import QtCore, QtGui, QtWidgets
from helper import WidgetData
from ui.aclmanager_ui import Ui_diag_aclmanager
from .helper import WidgetData
from .ui.aclmanager_ui import Ui_diag_aclmanager
class AclManager(QtWidgets.QDialog, Ui_diag_aclmanager):
@@ -85,7 +85,7 @@ class AclManager(QtWidgets.QDialog, Ui_diag_aclmanager):
)
) == QtWidgets.QMessageBox.Yes
if ask:
self.on_btn_add_pressed()
self.on_btn_add_clicked()
if self.__check_load_error():
return
@@ -252,11 +252,11 @@ class AclManager(QtWidgets.QDialog, Ui_diag_aclmanager):
self.btn_remove.setEnabled(not self.__read_only and selected_rows > 0)
@QtCore.pyqtSlot()
def on_btn_edit_pressed(self):
def on_btn_edit_clicked(self):
self.__load_selected_entry()
@QtCore.pyqtSlot()
def on_btn_remove_pressed(self):
def on_btn_remove_clicked(self):
lst_selected_row_indexes = [mi.row() for mi in self.tb_acls.selectionModel().selectedRows(0)]
if len(lst_selected_row_indexes) == 0:
return
@@ -355,7 +355,7 @@ class AclManager(QtWidgets.QDialog, Ui_diag_aclmanager):
self._check_all_filled()
@QtCore.pyqtSlot()
def on_btn_add_pressed(self):
def on_btn_add_clicked(self):
"""Add a new entry to acl table."""
ip_level = "{0}.{1}.{2}.{3},{4}".format(
self.txt_ip_a.text(),
@@ -366,7 +366,7 @@ class AclManager(QtWidgets.QDialog, Ui_diag_aclmanager):
)
if self.__re_ipacl.match(ip_level):
self.__table_add_acl(ip_level)
self.on_btn_clear_pressed()
self.on_btn_clear_clicked()
else:
QtWidgets.QMessageBox.critical(
self, self.tr("Error"), self.tr(
@@ -376,7 +376,7 @@ class AclManager(QtWidgets.QDialog, Ui_diag_aclmanager):
)
@QtCore.pyqtSlot()
def on_btn_clear_pressed(self):
def on_btn_clear_clicked(self):
"""Clear entry widgets."""
self.txt_ip_a.clear()
self.txt_ip_b.clear()

View File

@@ -0,0 +1,381 @@
# -*- coding: utf-8 -*-
"""Revolution Pi search with zeroconf."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
import webbrowser
from re import compile
from sys import platform
from PyQt5 import QtCore, QtGui, QtWidgets
from zeroconf import IPVersion, ServiceBrowser, Zeroconf
from . import helper
from . import proginit as pi
from .helper import RevPiSettings, WidgetData, all_revpi_settings
from .ui.avahisearch_ui import Ui_diag_search
class AvahiSearchThread(QtCore.QThread):
"""Search thread for Revolution Pi with installed RevPiPyLoad."""
added = QtCore.pyqtSignal(str, str, int, str, str)
removed = QtCore.pyqtSignal(str, str)
updated = QtCore.pyqtSignal(str, str, int, str, str)
def __init__(self, parent=None):
super(AvahiSearchThread, self).__init__(parent)
self._cycle_wait_ms = 1000
self.re_posix = compile(
r"(?P<ip>(\d{1,3}\.){3}\d{1,3}).*"
r"(?P<mac>([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
)
def remove_service(self, zeroconf: Zeroconf, conf_type: str, name: str) -> None:
"""Revolution Pi disappeared."""
pi.logger.debug("AvahiSearchThread.remove_service")
self.removed.emit(name, conf_type)
def add_service(self, zeroconf: Zeroconf, conf_type: str, name: str) -> None:
"""New Revolution Pi found."""
pi.logger.debug("AvahiSearchThread.add_service")
info = zeroconf.get_service_info(conf_type, name)
if not info:
return
for ip in info.parsed_addresses(IPVersion.V4Only):
self.added.emit(name, info.server, info.port, conf_type, ip)
def update_service(self, zeroconf: Zeroconf, conf_type: str, name: str) -> None:
"""New data of revolution pi"""
pi.logger.debug("AvahiSearchThread.add_service")
info = zeroconf.get_service_info(conf_type, name)
if not info:
return
for ip in info.parsed_addresses(IPVersion.V4Only):
self.updated.emit(name, info.server, info.port, conf_type, ip)
def run(self) -> None:
pi.logger.debug("Started zero conf discovery.")
zeroconf = Zeroconf()
revpi_browser = ServiceBrowser(zeroconf, "_revpipyload._tcp.local.", self)
while not self.isInterruptionRequested():
# Just hanging around :)
self.msleep(self._cycle_wait_ms)
zeroconf.close()
pi.logger.debug("Stopped zero conf discovery.")
class AvahiSearch(QtWidgets.QDialog, Ui_diag_search):
def __init__(self, parent=None):
super(AvahiSearch, self).__init__(parent)
self.setupUi(self)
# Global variables to let parent decide other actions
self.connect_settings = None
self.just_save = False
# Local variables
self.clipboard = QtGui.QGuiApplication.clipboard()
self._th_zero_conf = AvahiSearchThread(self)
self.tb_revpi.setColumnWidth(0, 250)
self.btn_connect.setEnabled(False)
self.btn_save.setEnabled(False)
self.restoreGeometry(helper.settings.value("avahisearch/geo", b''))
column_sizes = helper.settings.value("avahisearch/column_sizes", [], type=list)
if len(column_sizes) == self.tb_revpi.columnCount():
for i in range(self.tb_revpi.columnCount()):
self.tb_revpi.setColumnWidth(i, int(column_sizes[i]))
# Global context menus
self.cm_connect_actions = QtWidgets.QMenu(self)
self.cm_connect_actions.addAction(self.act_connect_ssh)
self.cm_connect_actions.addAction(self.act_connect_xmlrpc)
self.cm_quick_actions = QtWidgets.QMenu(self)
self.cm_quick_actions.addAction(self.act_connect)
self.cm_quick_actions.addAction(self.act_connect_ssh)
self.cm_quick_actions.addAction(self.act_connect_xmlrpc)
self.cm_quick_actions.addSeparator()
self.cm_quick_actions.addAction(self.act_open_pictory)
self.cm_quick_actions.addSeparator()
self.cm_quick_actions.addAction(self.act_copy_host)
self.cm_quick_actions.addAction(self.act_copy_ip)
self.tb_revpi.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.tb_revpi.customContextMenuRequested.connect(self._cm_quick_exec)
@QtCore.pyqtSlot(QtCore.QPoint)
def _cm_quick_exec(self, position: QtCore.QPoint) -> None:
selected_items = self.tb_revpi.selectedItems()
if not selected_items:
return
item = selected_items[0]
revpi_settings = bool(item.data(WidgetData.revpi_settings))
self.act_connect.setVisible(revpi_settings)
self.act_connect_ssh.setVisible(not revpi_settings)
self.act_connect_xmlrpc.setVisible(not revpi_settings)
sender = self.sender()
self.cm_quick_actions.exec(sender.mapToGlobal(position))
self.act_connect.setVisible(True)
self.act_connect_ssh.setVisible(True)
self.act_connect_xmlrpc.setVisible(True)
@staticmethod
def _find_settings(address: str) -> list[RevPiSettings]:
"""Find all settings with known avahi_id."""
return [
revpi_setting
for revpi_setting in all_revpi_settings()
if revpi_setting.address.lower() == address.lower()
]
def _restart_search(self) -> None:
"""Clean up and restart search thread."""
while self.tb_revpi.rowCount() > 0:
# Remove each row, a .clean would destroy the columns
self.tb_revpi.removeRow(0)
self._th_zero_conf.requestInterruption()
self._th_zero_conf = AvahiSearchThread(self)
self._th_zero_conf.added.connect(self.on_avahi_added)
self._th_zero_conf.updated.connect(self.on_avahi_added)
self._th_zero_conf.removed.connect(self.on_avahi_removed)
self._th_zero_conf.start()
def _create_settings_object(self, row: int, ssh_tunnel: bool) -> RevPiSettings or None:
"""
Create settings object from given row to settings.
:param row: Row with connection data
:param ssh_tunnel: Save as SSH tunnel connection
:return: RevPi settings with data from avahi search and default values
"""
item = self.tb_revpi.item(row, 0)
if not item:
return None
settings = RevPiSettings()
settings.folder = self.tr("Auto discovered")
settings.name = item.data(WidgetData.host_name)
settings.address = item.data(WidgetData.address)
settings.port = item.data(WidgetData.port)
settings.ssh_use_tunnel = ssh_tunnel
return settings
def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
helper.settings.setValue("avahisearch/geo", self.saveGeometry())
helper.settings.setValue("avahisearch/column_sizes", [
self.tb_revpi.columnWidth(i)
for i in range(self.tb_revpi.columnCount())
])
def exec(self) -> int:
self.connect_settings = None
self.just_save = False
self._restart_search()
rc = super(AvahiSearch, self).exec()
self._th_zero_conf.requestInterruption()
return rc
@QtCore.pyqtSlot()
def on_act_connect_triggered(self) -> None:
"""Connect via existing settings or ask for type."""
pi.logger.debug("AvahiSearch.on_act_connect_triggered")
selected_items = self.tb_revpi.selectedItems()
if not selected_items:
return
item = selected_items[0]
revpi_settings = item.data(WidgetData.revpi_settings) # type: RevPiSettings
if not revpi_settings:
return
self.connect_settings = revpi_settings
self.accept()
@QtCore.pyqtSlot()
def on_act_connect_ssh_triggered(self) -> None:
"""Create new revpi settings with ssh, save and connect."""
pi.logger.debug("AvahiSearch.on_act_connect_ssh_triggered")
if self.tb_revpi.currentRow() == -1:
return
revpi_settings = self._create_settings_object(self.tb_revpi.currentRow(), True)
revpi_settings.save_settings()
self.connect_settings = revpi_settings
self.accept()
@QtCore.pyqtSlot()
def on_act_connect_xmlrpc_triggered(self) -> None:
"""Create new revpi settings with XML-RPC, save and connect."""
pi.logger.debug("AvahiSearch.on_act_connect_xmlrpc_triggered")
if self.tb_revpi.currentRow() == -1:
return
revpi_settings = self._create_settings_object(self.tb_revpi.currentRow(), False)
revpi_settings.save_settings()
self.connect_settings = revpi_settings
self.accept()
@QtCore.pyqtSlot()
def on_act_copy_host_triggered(self) -> None:
"""Copy hostname of selected item to clipboard."""
selected_items = self.tb_revpi.selectedItems()
if not selected_items:
return
item = selected_items[0]
# Use just the hostname on Windows systems, it can not resolve .local addresses
self.clipboard.setText(
item.data(WidgetData.host_name) if platform == "win32"
else item.data(WidgetData.host_name_full)
)
@QtCore.pyqtSlot()
def on_act_copy_ip_triggered(self) -> None:
"""Copy ip address of selected item to clipboard."""
selected_items = self.tb_revpi.selectedItems()
if not selected_items:
return
item = selected_items[0]
self.clipboard.setText(item.data(WidgetData.address))
@QtCore.pyqtSlot()
def on_act_open_pictory_triggered(self) -> None:
"""Open piCtory in default browser of operating system."""
selected_items = self.tb_revpi.selectedItems()
if not selected_items:
return
item = selected_items[0]
# Till we could not choose https / http we are using the ip address
webbrowser.open("http://{0}/".format(item.data(WidgetData.address)))
@QtCore.pyqtSlot(str, str, int, str, str)
def on_avahi_added(self, avahi_id: str, server: str, port: int, conf_type: str, ip: str) -> None:
"""New Revolution Pi found."""
def update_tb_revpi_row(row_index: int):
host_name_full = server[:-1]
host_name = host_name_full[:host_name_full.find(".")]
item_name = self.tb_revpi.item(row_index, 0)
item_name.setData(WidgetData.address, ip)
item_name.setData(WidgetData.port, port)
item_name.setData(WidgetData.host_name_full, host_name_full)
item_name.setData(WidgetData.host_name, host_name)
revpi_settings = item_name.data(WidgetData.revpi_settings) # type: RevPiSettings
if revpi_settings:
# Generate the name of saved revpi and show the avahi-name in brackets
settings_text = "{0}/{1}".format(revpi_settings.folder, revpi_settings.name) \
if revpi_settings.folder \
else revpi_settings.name
if revpi_settings.ssh_use_tunnel:
settings_text += self.tr(" over SSH")
item_name.setText("{0} ({1})".format(settings_text, host_name))
else:
item_name.setText(host_name)
item_name.setToolTip(item_name.text())
item_ip = self.tb_revpi.item(row_index, 1)
item_ip.setText(ip)
item_ip.setToolTip(item_name.text())
lst_existing = self._find_settings(ip)
exists = False
for i in range(self.tb_revpi.rowCount()):
item_tb_revpi = self.tb_revpi.item(i, 0)
if item_tb_revpi.data(WidgetData.object_name) == avahi_id:
# Object already discovered
update_tb_revpi_row(i)
exists = True
if not exists:
for known_settings in lst_existing or [None]:
item_name = QtWidgets.QTableWidgetItem()
item_name.setIcon(QtGui.QIcon(":/main/ico/cpu.ico"))
item_name.setData(WidgetData.object_name, avahi_id)
item_name.setData(WidgetData.revpi_settings, known_settings)
index = self.tb_revpi.rowCount()
self.tb_revpi.insertRow(index)
self.tb_revpi.setItem(index, 0, item_name)
self.tb_revpi.setItem(index, 1, QtWidgets.QTableWidgetItem())
update_tb_revpi_row(index)
@QtCore.pyqtSlot(str, str)
def on_avahi_removed(self, avahi_id: str, conf_type: str) -> None:
"""Revolution Pi disappeared."""
for i in range(self.tb_revpi.rowCount()):
if self.tb_revpi.item(i, 0).data(WidgetData.object_name) == avahi_id:
self.tb_revpi.removeRow(i)
break
@QtCore.pyqtSlot(int, int)
def on_tb_revpi_cellDoubleClicked(self, row: int, column: int) -> None:
"""Connect to double-clicked Revolution Pi."""
pi.logger.debug("AvahiSearch.on_tb_revpi_cellDoubleClicked")
selected_items = self.tb_revpi.selectedItems()
if not selected_items:
return
item = selected_items[0]
revpi_settings = bool(item.data(WidgetData.revpi_settings))
if revpi_settings:
self.act_connect.trigger()
else:
cur = QtGui.QCursor()
self.cm_connect_actions.exec(cur.pos())
@QtCore.pyqtSlot(int, int, int, int)
def on_tb_revpi_currentCellChanged(self, row: int, column: int, last_row: int, last_column: int) -> None:
"""Manage state of buttons."""
self.btn_connect.setEnabled(row >= 0)
self.btn_save.setEnabled(row >= 0)
@QtCore.pyqtSlot()
def on_btn_connect_clicked(self) -> None:
"""Connect to selected Revolution Pi."""
pi.logger.debug("AvahiSearch.on_btn_connect_clicked")
selected_items = self.tb_revpi.selectedItems()
if not selected_items:
return
item = selected_items[0]
revpi_settings = bool(item.data(WidgetData.revpi_settings))
if revpi_settings:
self.act_connect.trigger()
else:
pos_context_menu = self.btn_connect.pos()
pos_context_menu.setY(pos_context_menu.y() + self.btn_connect.height())
self.cm_connect_actions.exec(self.mapToGlobal(pos_context_menu))
@QtCore.pyqtSlot()
def on_btn_save_clicked(self) -> None:
"""Save selected Revolution Pi."""
pi.logger.debug("AvahiSearch.on_btn_save_clicked")
row_index = self.tb_revpi.currentRow()
if row_index == -1:
return
self.connect_settings = self._create_settings_object(row_index, True)
self.just_save = True
self.accept()
@QtCore.pyqtSlot()
def on_btn_restart_clicked(self) -> None:
"""Clean up and restart search thread."""
self._restart_search()

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
"""File transfer system to handle QThreads."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 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.wait_interruptable(2)
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

View File

@@ -1,18 +1,18 @@
# -*- coding: utf-8 -*-
"""Debug control widget to append to main window."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2020 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
import pickle
from xmlrpc.client import Binary, Fault, MultiCall, MultiCallIterator
from PyQt5 import QtCore, QtWidgets
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
import proginit as pi
from debugios import DebugIos
from ui.debugcontrol_ui import Ui_wid_debugcontrol
from . import helper
from . import proginit as pi
from .debugios import DebugIos
from .ui.debugcontrol_ui import Ui_wid_debugcontrol
class PsValues(QtCore.QThread):
@@ -74,7 +74,17 @@ class DebugControl(QtWidgets.QWidget, Ui_wid_debugcontrol):
QtWidgets.QSpacerItem(20, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
)
self.cbx_write.setEnabled(False)
self.cbx_stay_on_top.setChecked(helper.settings.value("stay_on_top", False, bool))
self.cbx_stay_on_top.setChecked(helper.settings.value("debugcontrol/stay_on_top", False, bool))
self.shc_read_io = QtWidgets.QShortcut(QtGui.QKeySequence("F4"), self)
self.shc_read_io.setContext(QtCore.Qt.ApplicationShortcut)
self.shc_read_io.activated.connect(self.on_btn_read_io_pressed)
self.shc_refresh_io = QtWidgets.QShortcut(QtGui.QKeySequence("F5"), self)
self.shc_refresh_io.setContext(QtCore.Qt.ApplicationShortcut)
self.shc_refresh_io.activated.connect(self.on_btn_refresh_io_pressed)
self.shc_write_o = QtWidgets.QShortcut(QtGui.QKeySequence("F6"), self)
self.shc_write_o.setContext(QtCore.Qt.ApplicationShortcut)
self.shc_write_o.activated.connect(self.on_btn_write_o_clicked)
def __del__(self):
pi.logger.debug("DebugControl.__del__")
@@ -186,7 +196,7 @@ class DebugControl(QtWidgets.QWidget, Ui_wid_debugcontrol):
win = self.dict_windows[position]
for io in self.dict_ios[io_type][position]: # type: list
# ['name', bitlength, byte_address, 'bmk', bitaddress, 'byteorder', signed]
# ['name', bytelen, byte_address, 'bmk', bitaddress, 'byteorder', signed]
value_procimg = bytes(ba_values[io[2]:io[2] + io[1]])
if io[4] >= 0:
# Bit-IO
@@ -323,8 +333,6 @@ class DebugControl(QtWidgets.QWidget, Ui_wid_debugcontrol):
dict_inps[position], dict_outs[position]
)
win.device_closed.connect(self.on_device_closed)
win.do_read.connect(self.btn_refresh_io.pressed)
win.do_write.connect(self.btn_write_o.pressed)
self.dict_windows[position] = win
btn = QtWidgets.QPushButton(self.gb_devices)
@@ -342,7 +350,7 @@ class DebugControl(QtWidgets.QWidget, Ui_wid_debugcontrol):
self._work_values(refresh=True)
self._set_gui_control_states()
self.cbx_refresh.setChecked(helper.settings.value("auto_refresh", False, bool))
self.cbx_refresh.setChecked(helper.settings.value("debugcontrol/auto_refresh", False, bool))
return True
@@ -380,9 +388,9 @@ class DebugControl(QtWidgets.QWidget, Ui_wid_debugcontrol):
self._work_values(refresh=True)
@QtCore.pyqtSlot()
def on_btn_write_o_pressed(self):
def on_btn_write_o_clicked(self):
"""Write outputs."""
pi.logger.debug("DebugControl.on_btn_write_o_pressed")
pi.logger.debug("DebugControl.on_btn_write_o_clicked")
if not self.cbx_write.isChecked() and (helper.cm.xml_mode >= 3 or helper.cm.simulating):
for win in self.dict_windows.values(): # type: DebugIos
win.reset_label_colors()
@@ -415,12 +423,12 @@ class DebugControl(QtWidgets.QWidget, Ui_wid_debugcontrol):
@QtCore.pyqtSlot(bool)
def on_cbx_refresh_clicked(self, state: bool):
"""Save the state on user action."""
helper.settings.setValue("auto_refresh", state)
helper.settings.setValue("debugcontrol/auto_refresh", state)
@QtCore.pyqtSlot(bool)
def on_cbx_stay_on_top_clicked(self, state: bool):
"""Save the state on user action."""
helper.settings.setValue("stay_on_top", state)
helper.settings.setValue("debugcontrol/stay_on_top", state)
@QtCore.pyqtSlot(int)
def on_cbx_write_stateChanged(self, state: int):

View File

@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
"""One device of the Revolution Pi."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2020 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
import struct
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
import proginit as pi
from ui.debugios_ui import Ui_win_debugios
from . import helper
from . import proginit as pi
from .ui.debugios_ui import Ui_win_debugios
class DebugIos(QtWidgets.QMainWindow, Ui_win_debugios):
@@ -18,15 +18,14 @@ class DebugIos(QtWidgets.QMainWindow, Ui_win_debugios):
device_closed = QtCore.pyqtSignal(int)
"""This window was closed."""
do_read = QtCore.pyqtSignal()
do_write = QtCore.pyqtSignal()
search_class = (QtWidgets.QLineEdit, QtWidgets.QDoubleSpinBox, QtWidgets.QCheckBox)
def __init__(self, position: int, name: str, inputs: list, outputs: list, parent=None):
super(DebugIos, self).__init__(parent)
self.setupUi(self)
self.restoreGeometry(helper.cm.debug_geos.get(position, b''))
self.restoreGeometry(helper.cm.settings.debug_geos.get(position, b''))
self.setWindowTitle("{0} - {1}".format(position, name))
self.gb_io.setTitle(self.gb_io.title().format(name))
@@ -48,17 +47,12 @@ class DebugIos(QtWidgets.QMainWindow, Ui_win_debugios):
self._create_io(self.outputs, self.saw_out, False)
self.style_sheet = "background-color: red;"
shc_read = QtWidgets.QShortcut(QtGui.QKeySequence("F5"), self)
shc_read.activated.connect(self.do_read)
shc_write = QtWidgets.QShortcut(QtGui.QKeySequence("F6"), self)
shc_write.activated.connect(self.do_write)
def __del__(self):
pi.logger.debug("DebugIos.__del__")
def closeEvent(self, a0: QtGui.QCloseEvent):
pi.logger.debug("DebugIos.closeEvent")
helper.cm.debug_geos[self.position] = self.saveGeometry()
helper.cm.settings.debug_geos[self.position] = self.saveGeometry()
self.device_closed.emit(self.position)
@staticmethod
@@ -112,7 +106,8 @@ class DebugIos(QtWidgets.QMainWindow, Ui_win_debugios):
self.splitter.setSizes([1, 1])
def _create_widget(self, name: str, byte_length: int, bit_address: int, byteorder: str, signed: bool, read_only: bool):
def _create_widget(
self, name: str, byte_length: int, bit_address: int, byteorder: str, signed: bool, read_only: bool):
"""Create widget in functions address space to use lambda functions."""
if bit_address >= 0:
val = QtWidgets.QCheckBox()
@@ -339,19 +334,26 @@ class DebugIos(QtWidgets.QMainWindow, Ui_win_debugios):
:param value: New value as bytes or bool for widget
:param just_last_value: Just set last value property
"""
# child = self.findChild(self.search_class, io_name)
child = self.__qwa[io_name]
if child.property("frm"):
value = struct.unpack(child.property("frm"), value)[0]
elif type(value) == bytes:
if child.property("struct_type") == "text":
try:
value = value.decode("utf-8")
except UnicodeDecodeError:
child.setProperty("struct_type", "number")
QtWidgets.QMessageBox.warning(
self, self.tr("Can not use format text"), self.tr(
"Can not convert bytes {0} to a text for IO '{1}'. Switch to number format instead!"
).format(value, io_name)
)
if child.property("struct_type") == "number":
value = str(int.from_bytes(
value,
byteorder="big" if child.property("big_endian") else "little",
signed=child.property("signed") or False
))
else:
value = value.decode()
child.setProperty("last_value", value)
if not just_last_value:

View File

@@ -0,0 +1,692 @@
# -*- coding: utf-8 -*-
"""Helper functions for this application."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
import pickle
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 uuid import uuid4
from xmlrpc.client import Binary, ServerProxy
from PyQt5 import QtCore, QtWidgets
from paramiko.ssh_exception import AuthenticationException
from . import proginit as pi
from .ssh_tunneling.server import SSHLocalTunnel
from .sshauth import SSHAuth
settings = QtCore.QSettings("revpimodio.org", "RevPiCommander")
"""Global application settings."""
homedir = environ.get("HOME", "") or environ.get("APPDATA", "")
"""Home dir of user."""
class WidgetData(IntEnum):
address = 260
acl_level = 262
has_error = 263
port = 264
object_name = 265
host_name = 267
host_name_full = 268
file_name = 309
revpi_settings = 320
class RevPiSettings:
def __init__(self, load_index: int = None, settings_storage: QtCore.QSettings = None):
"""
Revolution Pi saved settings.
:param load_index: Load settings from index, same as .load_from_index
:param settings_storage: Change QSettings object to work on from default to this one
"""
self._settings = settings_storage or settings
self.internal_id = ""
self.name = "New connection"
self.folder = ""
self.address = "127.0.0.1"
self.port = 55123
self.timeout = 5
self.ssh_use_tunnel = True
self.ssh_port = 22
self.ssh_user = "pi"
self.ssh_saved_password = False
self.last_dir_upload = "."
self.last_file_upload = "."
self.last_dir_pictory = "."
self.last_dir_picontrol = "."
self.last_dir_selected = "."
self.last_pictory_file = ""
self.last_tar_file = ""
self.last_zip_file = ""
self.watch_files = []
self.watch_path = ""
self.debug_geos = {}
if load_index is not None:
self.load_from_index(load_index)
def load_from_index(self, settings_index: int) -> None:
"""Load settings from 'connections' index."""
self._settings.beginReadArray("connections")
self._settings.setArrayIndex(settings_index)
# Flag as "legacy" connection to generate missing internal_id on save_settings()
self.internal_id = self._settings.value("internal_id", "legacy", type=str)
self.name = self._settings.value("name", type=str)
self.folder = self._settings.value("folder", "", type=str)
self.address = self._settings.value("address", type=str)
self.port = self._settings.value("port", 55123, type=int)
self.timeout = self._settings.value("timeout", 5, type=int)
self.ssh_use_tunnel = self._settings.value("ssh_use_tunnel", True, type=bool)
self.ssh_port = self._settings.value("ssh_port", 22, type=int)
self.ssh_user = self._settings.value("ssh_user", "pi", type=str)
self.ssh_saved_password = self._settings.value("ssh_saved_password", False, type=bool)
self.last_dir_upload = self._settings.value("last_dir_upload", ".", type=str)
self.last_file_upload = self._settings.value("last_file_upload", ".", type=str)
self.last_dir_pictory = self._settings.value("last_dir_pictory", ".", type=str)
self.last_dir_picontrol = self._settings.value("last_dir_picontrol", ".", type=str)
self.last_dir_selected = self._settings.value("last_dir_selected", ".", type=str)
self.last_pictory_file = self._settings.value("last_pictory_file", "", type=str)
self.last_tar_file = self._settings.value("last_tar_file", "", type=str)
self.last_zip_file = self._settings.value("last_zip_file", "", type=str)
self.watch_files = self._settings.value("watch_files", [], type=list)
self.watch_path = self._settings.value("watch_path", "", type=str)
try:
# Bytes with QSettings are a little difficult sometimes
self.debug_geos = self._settings.value("debug_geos", {}, type=dict)
except Exception:
# Just drop the geos of IO windows
pass
# These values must exists
if not (self.name and self.address and self.port):
raise ValueError("Could not geht all required values from saved settings")
self._settings.endArray()
def save_settings(self):
"""Save all settings."""
count_settings = self._settings.beginReadArray("connections")
def create_new_array_member():
"""Insert a new setting at the end of the array."""
# Close the active array action to reopen a write action to expand the array
self._settings.endArray()
self._settings.beginWriteArray("connections")
self._settings.setArrayIndex(count_settings)
if not self.internal_id:
self.internal_id = uuid4().hex
if not self.internal_id:
create_new_array_member()
else:
# Always search setting in array, because connection manager could reorganize array indexes
new_setting = True
for index in range(count_settings):
self._settings.setArrayIndex(index)
if self.internal_id == "legacy":
# Legacy connection without internal_id
if self._settings.value("address") == self.address:
# Set missing internal_id
self.internal_id = uuid4().hex
new_setting = False
break
else:
if self._settings.value("internal_id") == self.internal_id:
new_setting = False
break
if new_setting:
# On this point, we iterate all settings and found none, so create new one
create_new_array_member()
self._settings.setValue("internal_id", self.internal_id)
self._settings.setValue("name", self.name)
self._settings.setValue("folder", self.folder)
self._settings.setValue("address", self.address)
self._settings.setValue("port", self.port)
self._settings.setValue("timeout", self.timeout)
self._settings.setValue("ssh_use_tunnel", self.ssh_use_tunnel)
self._settings.setValue("ssh_port", self.ssh_port)
self._settings.setValue("ssh_user", self.ssh_user)
self._settings.setValue("ssh_saved_password", self.ssh_saved_password)
self._settings.setValue("last_dir_upload", self.last_dir_upload)
self._settings.setValue("last_file_upload", self.last_file_upload)
self._settings.setValue("last_dir_pictory", self.last_dir_pictory)
self._settings.setValue("last_dir_picontrol", self.last_dir_picontrol)
self._settings.setValue("last_dir_selected", self.last_dir_selected)
self._settings.setValue("last_pictory_file", self.last_pictory_file)
self._settings.setValue("last_tar_file", self.last_tar_file)
self._settings.setValue("last_zip_file", self.last_zip_file)
self._settings.setValue("watch_files", self.watch_files)
self._settings.setValue("watch_path", self.watch_path)
self._settings.setValue("debug_geos", self.debug_geos)
self._settings.endArray()
class ConnectionManager(QtCore.QThread):
"""Check connection and status for PLC program on Revolution Pi."""
connection_established = QtCore.pyqtSignal()
"""New connection established successfully with <class 'ServerProxy'>."""
connection_disconnected = QtCore.pyqtSignal()
"""Connection disconnected."""
connection_disconnecting = QtCore.pyqtSignal()
"""Signal emitted before closing connection."""
connection_error_observed = QtCore.pyqtSignal(str)
"""This will be triggered, if a connection error was detected."""
status_changed = QtCore.pyqtSignal(str, str)
"""Status message and color suggestion."""
connection_recovered = QtCore.pyqtSignal()
"""After errors the connection is established again, could have other port information (SSH)."""
def __init__(self, parent=None, cycle_time_ms=1000):
super(ConnectionManager, self).__init__(parent)
self._cli = None
self._cli_connect = Queue()
self._cycle_time = cycle_time_ms
self._lck_cli = Lock()
self._ps_started = False
self._revpi = None
self._revpi_output = None
self.settings = RevPiSettings()
self.ssh_tunnel_server = None # type: SSHLocalTunnel
self.ssh_pass = ""
self.pyload_version = (0, 0, 0)
"""Version number of RevPiPyLoad 0.0.0 with <class 'int'> values."""
self.xml_funcs = []
"""Name list of all supported functions of RevPiPyLoad."""
self.xml_mode = -1
"""ACL level for this connection (-1 on connection denied)."""
self._xml_mode_refresh = False
def __call_simulator(self, function_name: str, *args, default_value=None, **kwargs):
pi.logger.debug("ConnectionManager.__call_simulator({0})".format(function_name))
if function_name == "ps_values":
if self._revpi.readprocimg():
bytebuff = bytearray()
for dev in self._revpi.device:
bytebuff += bytes(dev)
return Binary(bytes(bytebuff))
elif function_name == "ps_setvalue":
# args: 0=device, 1=io, 2=value
device = args[0]
io = args[1]
if type(args[2]) == Binary:
value = args[2].data
else:
value = args[2]
try:
# Write new value to IO
self._revpi.io[io].set_value(value)
except Exception as e:
return [device, io, False, str(e)]
return [device, io, True, ""]
elif function_name == "psstart":
self._revpi.autorefresh_all()
return True
elif function_name == "psstop":
self._revpi.exit(full=False)
return True
elif function_name == "ps_devices":
return [(dev.position, dev.name) for dev in self._revpi.device]
elif function_name == "ps_inps":
return self.__simulator_ios("inp")
elif function_name == "ps_outs":
return self.__simulator_ios("out")
else:
return default_value
def __simulator_ios(self, iotype: str):
dict_ios = {}
for dev in self._revpi.device:
dict_ios[dev.position] = []
if iotype == "inp":
lst_io = dev.get_inputs()
elif iotype == "out":
lst_io = dev.get_outputs()
else:
lst_io = []
for io in lst_io:
dict_ios[dev.position].append([
io.name,
1 if io._bitlength == 1 else int(io._bitlength / 8),
io._slc_address.start + dev.offset,
io.bmk,
io._bitaddress,
io._byteorder,
io._signed,
])
return Binary(pickle.dumps(dict_ios))
def _clear_settings(self):
"""Clear connection settings."""
self.settings = RevPiSettings()
self.ssh_pass = ""
self.pyload_version = (0, 0, 0)
self.xml_funcs.clear()
self.xml_mode = -1
def pyload_connect(self, revpi_settings: RevPiSettings, parent=None) -> bool:
"""
Create a new connection from settings object.
:param revpi_settings: Revolution Pi saved connection settings
:param parent: Qt parent window for dialog positioning
:return: True, if the connection was successfully established
"""
# First disconnect to send signal and clean up values
self.pyload_disconnect()
ssh_tunnel_server = None
ssh_tunnel_port = 0
ssh_pass = ""
socket.setdefaulttimeout(2)
if revpi_settings.ssh_use_tunnel:
while True:
diag_ssh_auth = SSHAuth(
revpi_settings.ssh_user,
"{0}.{1}_{2}".format(
settings.applicationName(),
settings.organizationName(),
revpi_settings.internal_id),
parent,
)
if not diag_ssh_auth.exec() == QtWidgets.QDialog.Accepted:
self._clear_settings()
return False
ssh_user = diag_ssh_auth.username
ssh_pass = diag_ssh_auth.password
ssh_tunnel_server = SSHLocalTunnel(
revpi_settings.port,
revpi_settings.address,
revpi_settings.ssh_port
)
revpi_settings.ssh_saved_password = diag_ssh_auth.in_keyring
try:
ssh_tunnel_port = ssh_tunnel_server.connect_by_credentials(ssh_user, ssh_pass)
break
except AuthenticationException:
diag_ssh_auth.remove_saved_password()
QtWidgets.QMessageBox.critical(
parent, self.tr("Error"), self.tr(
"The combination of username and password was rejected from the SSH server.\n\n"
"Try again."
)
)
except Exception as e:
# todo: Check some more kinds of exceptions and nice user info
self._clear_settings()
QtWidgets.QMessageBox.critical(
parent, self.tr("Error"), self.tr(
"Could not establish a SSH connection to server:\n\n{0}"
).format(str(e))
)
return False
sp = ServerProxy("http://127.0.0.1:{0}".format(ssh_tunnel_port))
else:
sp = ServerProxy("http://{0}:{1}".format(revpi_settings.address, revpi_settings.port))
# Load values and test connection to Revolution Pi
try:
pyload_version = tuple(map(int, sp.version().split(".")))
xml_funcs = sp.system.listMethods()
xml_mode = sp.xmlmodus()
except Exception as e:
pi.logger.exception(e)
self.connection_error_observed.emit(str(e))
if not revpi_settings.ssh_use_tunnel:
# todo: Change message, that user can use ssh
QtWidgets.QMessageBox.critical(
parent, self.tr("Error"), self.tr(
"Can not connect to RevPi XML-RPC Service! \n\n"
"This could have the following reasons: The RevPi is not "
"online, the XML-RPC service is not running / bind to "
"localhost or the ACL permission is not set for your "
"IP!!!\n\nRun 'sudo revpipyload_secure_installation' on "
"Revolution Pi to setup this function!"
)
)
return False
self.settings = revpi_settings
self.ssh_pass = ssh_pass
self.pyload_version = pyload_version
self.xml_funcs = xml_funcs
self.xml_mode = xml_mode
with self._lck_cli:
socket.setdefaulttimeout(revpi_settings.timeout)
self.ssh_tunnel_server = ssh_tunnel_server
self._cli = sp
self._cli_connect.put_nowait((
"127.0.0.1" if revpi_settings.ssh_use_tunnel else revpi_settings.address,
ssh_tunnel_port if revpi_settings.ssh_use_tunnel else revpi_settings.port
))
self.connection_established.emit()
return True
def pyload_disconnect(self):
"""Disconnect from Revolution Pi."""
if self._revpi is not None:
self.connection_disconnecting.emit()
self._revpi.cleanup()
self._revpi_output.cleanup()
if settings.value("simulator/stop_remove", False, bool):
remove(self._revpi.procimg)
self._revpi = None
self._revpi_output = None
pi.logger.debug("Simulator destroyed.")
self.connection_disconnected.emit()
elif self._cli is not None:
# Tell all widget, that we want to disconnect
self.connection_disconnecting.emit()
self.settings.save_settings()
with self._lck_cli:
if self._ps_started:
try:
self._cli.psstop()
except Exception:
pass
self._clear_settings()
self._cli = None
if self.ssh_tunnel_server:
self.ssh_tunnel_server.disconnect()
self.ssh_tunnel_server = None
self.connection_disconnected.emit()
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")
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 for outputs
self._revpi_output = revpimodio2.RevPiModIO(configrsc=configrsc, procimg=procimg)
self._revpi_output.setdefaultvalues()
self._revpi_output.writeprocimg()
# This is our simulator to work with
self._revpi = revpimodio2.RevPiModIO(simulator=True, configrsc=configrsc, procimg=procimg)
self._revpi.setdefaultvalues()
self._revpi.writeprocimg()
self.xml_funcs = ["psstart", "psstop", "ps_devices", "ps_inps", "ps_outs", "ps_values", "ps_setvalue"]
self.connection_established.emit()
except Exception as e:
pi.logger.exception(e)
self.connection_error_observed.emit(str(e))
self._revpi_output = None
self._revpi = None
if settings.value("simulator/stop_remove", False, bool):
remove(procimg)
return self._revpi is not None
def refresh_xml_mode(self):
"""Refresh XML ACL level after some change could be done."""
self._xml_mode_refresh = True
def reset_simulator(self):
"""Reset all io to piCtory defaults."""
pi.logger.debug("ConnectionManager.reset_simulator")
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."""
self.setPriority(QtCore.QThread.NormalPriority)
sp = None
while not self.isInterruptionRequested():
if self._revpi is not None:
sp = None
self.status_changed.emit(self.tr("SIMULATING"), "yellow")
elif self._cli is None:
sp = None
self.status_changed.emit(self.tr("NOT CONNECTED"), "lightblue")
elif not self._cli_connect.empty():
# Get new connection information to create object in this thread
item = self._cli_connect.get()
sp = ServerProxy("http://{0}:{1}".format(*item))
self._cli_connect.task_done()
if sp:
try:
plc_exit_code = sp.plcexitcode()
if self._xml_mode_refresh:
self.xml_mode = sp.xmlmodus()
self._xml_mode_refresh = False
except CannotSendRequest as e:
pi.logger.warning(e)
except Exception as e:
pi.logger.warning(e)
self.status_changed.emit(self.tr("SERVER ERROR"), "red")
self.connection_error_observed.emit("{0} | {1}".format(e, type(e)))
if self.ssh_tunnel_server and not self.ssh_tunnel_server.connected:
self.ssh_tunnel_server.disconnect()
ssh_tunnel_server = SSHLocalTunnel(
self.settings.port,
self.settings.address,
self.settings.ssh_port
)
try:
ssh_tunnel_port = self.ssh_tunnel_server.connect_by_credentials(
self.settings.ssh_user,
self.ssh_pass
)
sp = ServerProxy("http://127.0.0.1:{0}".format(ssh_tunnel_port))
with self._lck_cli:
self.ssh_tunnel_server = ssh_tunnel_server
self._cli = sp
self.connection_recovered.emit()
except Exception:
pass
else:
if plc_exit_code == -1:
self.status_changed.emit(self.tr("RUNNING"), "green")
elif plc_exit_code == -2:
self.status_changed.emit(self.tr("PLC FILE NOT FOUND"), "red")
elif plc_exit_code == -3:
self.status_changed.emit(self.tr("NOT RUNNING (NO STATUS)"), "yellow")
elif plc_exit_code == -9:
self.status_changed.emit(self.tr("PROGRAM KILLED"), "red")
elif plc_exit_code == -15:
self.status_changed.emit(self.tr("PROGRAM TERMED"), "red")
elif plc_exit_code == 0:
self.status_changed.emit(self.tr("NOT RUNNING"), "yellow")
else:
self.status_changed.emit(self.tr("FINISHED WITH CODE {0}").format(plc_exit_code), "yellow")
self.msleep(self._cycle_time)
def call_remote_function(self, function_name: str, *args, default_value=None, raise_exception=False, **kwargs):
"""
Save call of a remote function with given name and parameters on Revolution Pi.
:param function_name: Function to call on RevPiPyLoad
:param args: Functions arguments
:param default_value: Default value will be returned on error
:param raise_exception: Will raise the exception returned from server
:param kwargs: Functions key word arguments
:return: Return value of remote function or default_value
"""
if self._cli is None and self._revpi is None:
pi.logger.error("Not connected while calling {0}".format(function_name))
if raise_exception:
raise ConnectionError("Connection manager not connected")
return default_value
reload_funcs = False
if function_name == "psstart":
self._ps_started = True
reload_funcs = True
elif function_name == "psstop":
self._ps_started = False
reload_funcs = True
# On connection problems do not freeze
if self._lck_cli.acquire(timeout=1.0):
if self._revpi is not None:
# Redirect call to simulator
return_value = self.__call_simulator(function_name, *args, default_value=default_value, **kwargs)
else:
try:
return_value = getattr(self._cli, function_name)(*args, **kwargs)
if reload_funcs:
self.xml_funcs = self._cli.system.listMethods()
except Exception as e:
pi.logger.error(e)
if raise_exception:
self._lck_cli.release()
raise
return_value = default_value
self._lck_cli.release()
return return_value
elif raise_exception:
raise ConnectionError("Can not get lock of connection")
return default_value
def get_cli(self):
"""
Connection proxy of actual connection.
Use connection_recovered signal to figure out new parameters.
"""
if not self.settings.ssh_use_tunnel and self.settings.address and self.settings.port:
return ServerProxy("http://{0}:{1}".format(self.settings.address, self.settings.port))
if self.settings.ssh_use_tunnel and self.ssh_tunnel_server and self.ssh_tunnel_server.connected:
return ServerProxy("http://127.0.0.1:{0}".format(self.ssh_tunnel_server.local_tunnel_port))
return None
@property
def connected(self) -> bool:
"""True if we have an active connection."""
return self._cli is not None
@property
def simulating(self) -> bool:
"""True, if simulating mode is running."""
return self._revpi is not None
@property
def simulating_configrsc(self) -> str:
return self._revpi.configrsc if self._revpi else ""
@property
def simulating_procimg(self) -> str:
return self._revpi.procimg if self._revpi else ""
cm = ConnectionManager()
"""Clobal connection manager instance."""
def all_revpi_settings() -> [RevPiSettings]:
"""Get all revpi settings objects."""
# Get length of array and close it, the RevPiSettings-class need it
count_settings = settings.beginReadArray("connections")
settings.endArray()
return [RevPiSettings(i) for i in range(count_settings)]
def import_old_settings():
"""Try to import saved connections from old storage to new setting object."""
if settings.value("revpicommander/imported_settings", False, type=bool):
return
settings.setValue("revpicommander/imported_settings", True)
old_settings = QtCore.QSettings("revpipyplc", "revpipyload")
count_settings = old_settings.beginReadArray("connections")
old_settings.endArray()
for i in range(count_settings):
try:
revpi_setting = RevPiSettings(i, settings_storage=old_settings)
revpi_setting._settings = settings
revpi_setting.save_settings()
except Exception as e:
pi.logger.warning("Could not import saved connection {0}".format(i))
import_old_settings()

Binary file not shown.

View File

@@ -2,13 +2,13 @@
"""Options for MQTT system."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from PyQt5 import QtGui, QtWidgets
import proginit as pi
from ui.mqttmanager_ui import Ui_diag_mqtt
from . import proginit as pi
from .ui.mqttmanager_ui import Ui_diag_mqtt
class MqttManager(QtWidgets.QDialog, Ui_diag_mqtt):

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Global program initialization."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2019 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "LGPLv3"
import logging
@@ -85,11 +85,6 @@ parser = ArgumentParser(
prog=programname,
description="Program description"
)
# parser.add_argument(
# "-c", "--conffile", dest="conffile",
# default="{0}.conf".format(programname),
# help="Application configuration file"
# )
parser.add_argument(
"-f", "--logfile", dest="logfile",
help="Save log entries to this file"

View File

@@ -5,24 +5,26 @@
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__license__ = "GPLv3"
__version__ = "0.9.0"
__version__ = "0.9.10rc2"
import webbrowser
from os.path import basename, dirname, join
from os.path import dirname, join
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
import proginit as pi
import revpilogfile
from avahisearch import AvahiSearch
from debugcontrol import DebugControl
from revpifiles import RevPiFiles
from revpiinfo import RevPiInfo
from revpioption import RevPiOption
from revpiplclist import RevPiPlcList
from revpiprogram import RevPiProgram
from ui.revpicommander_ui import Ui_win_revpicommander
from . import helper
from . import proginit as pi
from . import revpilogfile
from .avahisearch import AvahiSearch
from .debugcontrol import DebugControl
from .helper import RevPiSettings
from .revpifiles import RevPiFiles
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
class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
@@ -52,7 +54,7 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
self.win_files = RevPiFiles(self)
self.win_log = revpilogfile.RevPiLogfile(self)
self.btn_plc_logs.pressed.connect(self.on_act_logs_triggered)
self.btn_plc_logs.clicked.connect(self.on_act_logs_triggered)
helper.cm.connection_disconnected.connect(self.on_cm_connection_disconnected)
helper.cm.connection_disconnecting.connect(self.on_cm_connection_disconnecting)
@@ -60,29 +62,37 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
helper.cm.connection_error_observed.connect(self.on_cm_connection_error_observed)
helper.cm.status_changed.connect(self.on_cm_status_changed)
self.restoreGeometry(helper.settings.value("geo", b''))
self.restoreGeometry(helper.settings.value("revpicommander/geo", b''))
self.setWindowFlag(QtCore.Qt.WindowMaximizeButtonHint, False)
def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
pi.logger.debug("RevPiCommander.closeEvent")
helper.cm.pyload_disconnect()
helper.settings.setValue("geo", self.saveGeometry())
helper.settings.setValue("revpicommander/geo", self.saveGeometry())
def _set_gui_control_states(self):
"""Setup states of actions and buttons."""
connected = helper.cm.connected
self.men_plc.setEnabled(connected)
self.act_logs.setEnabled(connected)
self.act_options.setEnabled(connected and helper.cm.xml_mode >= 2)
self.act_program.setEnabled(connected and helper.cm.xml_mode >= 2)
self.act_developer.setEnabled(connected and helper.cm.xml_mode >= 3)
self.act_pictory.setEnabled(connected)
self.act_reset.setEnabled(connected and helper.cm.xml_mode >= 3)
self.act_disconnect.setEnabled(connected)
self.btn_plc_start.setEnabled(connected)
self.btn_plc_stop.setEnabled(connected)
self.btn_plc_restart.setEnabled(connected)
self.btn_plc_logs.setEnabled(connected)
self.btn_plc_debug.setEnabled(connected and helper.cm.xml_mode >= 1)
if helper.cm.simulating:
self.btn_plc_stop.setEnabled(True) # Stop simulator
self.btn_plc_restart.setEnabled(True) # Reset simulator values
self.btn_plc_debug.setEnabled(True)
else:
connected = helper.cm.connected
self.men_plc.setEnabled(connected)
self.act_logs.setEnabled(connected)
self.act_options.setEnabled(connected and helper.cm.xml_mode >= 2)
self.act_program.setEnabled(connected and helper.cm.xml_mode >= 2)
self.act_developer.setEnabled(connected and helper.cm.xml_mode >= 3)
self.act_pictory.setEnabled(connected)
self.act_reset.setEnabled(connected and helper.cm.xml_mode >= 3)
self.act_disconnect.setEnabled(connected)
self.btn_plc_start.setEnabled(connected)
self.btn_plc_stop.setEnabled(connected)
self.btn_plc_restart.setEnabled(connected)
self.btn_plc_logs.setEnabled(connected)
self.btn_plc_debug.setEnabled(connected and helper.cm.xml_mode >= 1)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# region # REGION: Connection management
@@ -102,6 +112,8 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
pi.logger.debug("RevPiCommander.on_cm_connection_disconnected")
self._set_gui_control_states()
self.txt_host.setVisible(True)
self.txt_host.clear()
self.txt_connection.clear()
@QtCore.pyqtSlot()
@@ -127,11 +139,15 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
pi.logger.debug("RevPiCommander.on_cm_connection_established")
self._set_gui_control_states()
self.txt_connection.setText("{0} - {1}:{2}".format(
helper.cm.name,
helper.cm.address,
helper.cm.port
))
if helper.cm.simulating:
self.txt_host.setVisible(False)
self.txt_connection.setText("configrsc=\"{0}\", procimg=\"{1}\"".format(
helper.cm.simulating_configrsc,
helper.cm.simulating_procimg,
))
else:
self.txt_host.setText(helper.cm.settings.name)
self.txt_connection.setText(helper.cm.settings.address)
self.win_files = RevPiFiles(self)
@QtCore.pyqtSlot(str, str)
@@ -151,29 +167,26 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
self.men_connections.clear()
self.dict_men_connections_subfolder.clear()
for i in range(helper.settings.beginReadArray("connections")):
helper.settings.setArrayIndex(i)
act = QtWidgets.QAction(self)
act.setText(helper.settings.value("name"))
act.setData(i)
act.setToolTip("{0}:{1}".format(
helper.settings.value("address"),
helper.settings.value("port"),
))
if helper.settings.value("folder"):
if helper.settings.value("folder") not in self.dict_men_connections_subfolder:
men_sub = QtWidgets.QMenu(self)
men_sub.setTitle(helper.settings.value("folder"))
self.dict_men_connections_subfolder[helper.settings.value("folder")] = men_sub
for settings in helper.all_revpi_settings(): # type: RevPiSettings
if settings.folder:
if settings.folder not in self.dict_men_connections_subfolder:
men_sub = QtWidgets.QMenu(self.men_connections)
men_sub.setTitle(settings.folder)
self.dict_men_connections_subfolder[settings.folder] = men_sub
self.men_connections.addMenu(men_sub)
self.dict_men_connections_subfolder[helper.settings.value("folder")].addAction(act)
parent_menu = self.dict_men_connections_subfolder[settings.folder]
else:
self.men_connections.addAction(act)
parent_menu = self.men_connections
helper.settings.endArray()
display_name = settings.name
if settings.ssh_use_tunnel:
display_name += " (SSH)"
act = QtWidgets.QAction(parent_menu)
act.setText(display_name)
act.setData(settings)
act.setToolTip("{0}:{1}".format(settings.address, settings.port))
parent_menu.addAction(act)
@QtCore.pyqtSlot()
def on_act_connections_triggered(self):
@@ -185,60 +198,45 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
def on_act_search_triggered(self):
"""Search for Revolution Pi with zero conf."""
if self.diag_search.exec() == QtWidgets.QDialog.Accepted:
if self.diag_search.connect_index >= 0:
helper.cm.pyload_connect(self.diag_search.connect_index)
if self.diag_search.connect_settings:
if self.diag_search.just_save:
self.diag_connections.exec_with_presets(self.diag_search.connect_settings)
else:
helper.cm.pyload_connect(self.diag_search.connect_settings, self)
self._load_men_connections()
@QtCore.pyqtSlot()
def on_act_simulator_triggered(self):
"""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)
helper.cm.pyload_disconnect()
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 "
"RevPiModIO with this additional parameters:\nprocimg={procimg}\nconfigrsc={config}\n\n"
"RevPiModIO with this additional parameters:\nprocimg={0}\nconfigrsc={1}\n\n"
"You can copy that from header textbox."
).format(procimg=procimg_file, config=configrsc_file)
).format(procimg_file, configrsc_file)
)
self.txt_connection.setText(
"configrsc=\"{0}\", procimg=\"{1}\"".format(configrsc_file, procimg_file)
)
# Stop button will disable simulating
self.btn_plc_stop.setEnabled(True)
self.btn_plc_restart.setEnabled(True)
self.btn_plc_debug.setEnabled(True)
else:
pi.logger.error("Can not start simulator")
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."""
@@ -249,8 +247,7 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
QtWidgets.QMessageBox.warning(
self, self.tr("Warning"), self.tr(
"This version of Logviewer ist not supported in version {0} "
"of RevPiPyLoad on your RevPi! You need at least version "
"0.4.1."
"of RevPiPyLoad on your RevPi! You need at least version 0.4.1."
).format(helper.cm.call_remote_function("version", default_value="-"))
)
return None
@@ -364,17 +361,7 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
@QtCore.pyqtSlot(QtWidgets.QAction)
def on_men_connections_triggered(self, action: QtWidgets.QAction):
"""A connection is selected in the men_connections menu."""
if not helper.cm.pyload_connect(action.data()):
QtWidgets.QMessageBox.critical(
self, self.tr("Error"), self.tr(
"Can not connect to RevPi XML-RPC Service! \n\n"
"This could have the following reasons: The RevPi is not "
"online, the XML-RPC service is not running / bind to "
"localhost or the ACL permission is not set for your "
"IP!!!\n\nRun 'sudo revpipyload_secure_installation' on "
"Revolution Pi to setup this function!"
)
)
helper.cm.pyload_connect(action.data(), self)
@QtCore.pyqtSlot()
def on_act_webpage_triggered(self):
@@ -410,10 +397,12 @@ 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(
self.tr("zero") if helper.settings.value("simulator/restart_zero", False, bool)
else self.tr("piCtory default"))
) == QtWidgets.QMessageBox.Yes
if rc:
# Set piCtory default values in process image
@@ -456,7 +445,7 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
debugcontrol = DebugControl(self.centralwidget)
if debugcontrol.reload_devices():
self.wid_debugcontrol = debugcontrol
self.gl.addWidget(self.wid_debugcontrol, 7, 0)
self.gl.addWidget(self.wid_debugcontrol)
else:
debugcontrol.deleteLater()
QtWidgets.QMessageBox.critical(
@@ -474,19 +463,23 @@ class RevPiCommander(QtWidgets.QMainWindow, Ui_win_revpicommander):
# endregion # # # # #
if __name__ == "__main__":
import sys
def main() -> int:
from sys import argv
#QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
#QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
if hasattr(QtCore.Qt, 'AA_EnableHighDpiScaling'):
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'):
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
app = QtWidgets.QApplication(sys.argv)
app = QtWidgets.QApplication(argv)
try:
# Setup translation from file with system language
locale = QtCore.QLocale.system().name()
translator = QtCore.QTranslator()
translator.load("locale/revpicommander_{0}".format(locale), suffix=".qm")
translator.load(
QtCore.QLocale.system(),
"revpicommander", "_", join(dirname(__file__), "locale"), ".qm"
)
app.installTranslator(translator)
except Exception:
pass
@@ -496,10 +489,16 @@ if __name__ == "__main__":
win = RevPiCommander()
win.show()
exit_code = app.exec_()
exit_code = app.exec()
# Clean up workers
helper.cm.requestInterruption()
helper.cm.wait()
sys.exit(exit_code)
return exit_code
if __name__ == "__main__":
import sys
sys.exit(main())

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""File manager for up und download PLC program."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2020 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
import gzip
@@ -11,10 +11,11 @@ from xmlrpc.client import Binary
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
import proginit as pi
from helper import WidgetData
from ui.files_ui import Ui_win_files
from . import helper
from . import proginit as pi
from .backgroundworker import BackgroundWorker
from .helper import WidgetData
from .ui.files_ui import Ui_win_files
class NodeType(IntEnum):
@@ -22,6 +23,56 @@ 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.settings.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):
@@ -31,7 +82,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
self.dc_settings = {}
self.tree_files_counter = 0
self.tree_files_counter_max = 10000
self.lbl_path_local.setText(helper.cm.develop_watch_path or self.tr("Please select..."))
self.lbl_path_local.setText(helper.cm.settings.watch_path or self.tr("Please select..."))
self.lbl_path_local.setToolTip(self.lbl_path_local.text())
self.btn_all.setEnabled(False)
@@ -39,7 +90,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
self.btn_to_right.setEnabled(False)
self.btn_delete_revpi.setEnabled(False)
if helper.cm.develop_watch_path:
if helper.cm.settings.watch_path:
self._load_files_local(True)
if helper.cm.connected:
self._load_files_revpi(True)
@@ -72,41 +123,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 +138,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 +146,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")
@@ -152,7 +175,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
if "plcdownload_file" not in helper.cm.xml_funcs:
self.btn_to_left.setEnabled(False)
self.btn_to_left.setToolTip(self.tr("The RevPiPyLoad version on the Revolution Pi is to old."))
elif not helper.cm.develop_watch_path:
elif not helper.cm.settings.watch_path:
self.btn_to_left.setEnabled(False)
self.btn_to_left.setToolTip(self.tr("Choose a local directory first."))
else:
@@ -184,7 +207,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
elif item.type() == NodeType.FILE:
item.setSelected(value)
def __item_selection_changed(self, tree_view: QtWidgets.QTreeView):
def __item_selection_changed(self, tree_view: QtWidgets.QTreeWidget):
"""Manager vor item selection of three views."""
item = tree_view.currentItem()
if item is None:
@@ -207,7 +230,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
@QtCore.pyqtSlot()
def on_tree_files_local_itemSelectionChanged(self):
self.__item_selection_changed(self.tree_files_local)
helper.cm.develop_watch_files = self.file_list_local()
helper.cm.settings.watch_files = self.file_list_local()
@QtCore.pyqtSlot()
def on_tree_files_revpi_itemSelectionChanged(self):
@@ -264,7 +287,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
else:
self.tree_files_local.addTopLevelItem(item)
item.setSelected(de.path in helper.cm.develop_watch_files)
item.setSelected(de.path in helper.cm.settings.watch_files)
self._parent_selection_state(item)
def _load_files_local(self, silent=False):
@@ -278,7 +301,7 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
self.tree_files_counter = 0
self.tree_files_local.blockSignals(True)
self.tree_files_local.clear()
self.__insert_files_local(helper.cm.develop_watch_path)
self.__insert_files_local(helper.cm.settings.watch_path)
self.tree_files_local.sortItems(0, QtCore.Qt.AscendingOrder)
self.tree_files_local.blockSignals(False)
@@ -411,18 +434,18 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
# endregion # # # # #
@QtCore.pyqtSlot()
def on_btn_all_pressed(self):
pi.logger.debug("RevPiFiles.on_btn_all_pressed")
def on_btn_all_clicked(self):
pi.logger.debug("RevPiFiles.on_btn_all_clicked")
self._do_my_job(True)
self.file_list_revpi()
@QtCore.pyqtSlot()
def on_btn_select_local_pressed(self):
pi.logger.debug("RevPiFiles.on_btn_select_pressed")
def on_btn_select_local_clicked(self):
pi.logger.debug("RevPiFiles.on_btn_select_clicked")
diag_folder = QtWidgets.QFileDialog(
self, self.tr("Select folder..."),
helper.cm.develop_watch_path,
helper.cm.settings.watch_path,
)
diag_folder.setFileMode(QtWidgets.QFileDialog.DirectoryOnly)
if diag_folder.exec() != QtWidgets.QFileDialog.Accepted:
@@ -436,38 +459,38 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
"Can not access the folder '{0}' to read files."
)
)
helper.cm.develop_watch_files = []
helper.cm.develop_watch_path = ""
helper.cm.settings.watch_files = []
helper.cm.settings.watch_path = ""
return
self.lbl_path_local.setText(selected_dir)
self.lbl_path_local.setToolTip(self.lbl_path_local.text())
helper.cm.develop_watch_path = selected_dir
helper.cm.develop_watch_files = []
helper.cm.settings.watch_path = selected_dir
helper.cm.settings.watch_files = []
self._load_files_local(False)
@QtCore.pyqtSlot()
def on_btn_refresh_local_pressed(self):
pi.logger.debug("RevPiFiles.on_btn_refresh_pressed")
def on_btn_refresh_local_clicked(self):
pi.logger.debug("RevPiFiles.on_btn_refresh_clicked")
self._load_files_local(False)
@QtCore.pyqtSlot()
def on_btn_refresh_revpi_pressed(self):
pi.logger.debug("RevPiFiles.on_btn_refresh_revpi_pressed")
def on_btn_refresh_revpi_clicked(self):
pi.logger.debug("RevPiFiles.on_btn_refresh_revpi_clicked")
self._load_files_revpi(False)
@QtCore.pyqtSlot()
def on_btn_to_right_pressed(self):
def on_btn_to_right_clicked(self):
"""Upload selected files to revolution pi."""
pi.logger.debug("RevPiFiles.on_btn_to_right_pressed")
pi.logger.debug("RevPiFiles.on_btn_to_right_clicked")
self._do_my_job(False)
self._load_files_revpi(True)
@QtCore.pyqtSlot()
def on_btn_to_left_pressed(self):
def on_btn_to_left_clicked(self):
"""Download selected file."""
pi.logger.debug("RevPiFiles.on_btn_to_left_pressed")
pi.logger.debug("RevPiFiles.on_btn_to_left_clicked")
override = None
for item in self.tree_files_revpi.selectedItems():
@@ -487,18 +510,18 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
).format(file_name)
)
else:
file_name = os.path.join(helper.cm.develop_watch_path, file_name)
file_name = os.path.join(helper.cm.settings.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))
@@ -506,15 +529,15 @@ class RevPiFiles(QtWidgets.QMainWindow, Ui_win_files):
os.makedirs(os.path.dirname(file_name), exist_ok=True)
file_data = gzip.decompress(rc)
with open(os.path.join(helper.cm.develop_watch_path, file_name), "wb") as fh:
with open(os.path.join(helper.cm.settings.watch_path, file_name), "wb") as fh:
fh.write(file_data)
self._load_files_local()
@QtCore.pyqtSlot()
def on_btn_delete_revpi_pressed(self):
def on_btn_delete_revpi_clicked(self):
"""Remove selected files from working directory on revolution pi."""
pi.logger.debug("RevPiFiles.btn_delete_revpi_pressed")
pi.logger.debug("RevPiFiles.btn_delete_revpi_clicked")
lst_delete = []
for item in self.tree_files_revpi.selectedItems():

View File

@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
"""Program information of local an remote system."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
from ui.revpiinfo_ui import Ui_diag_revpiinfo
from . import helper
from .ui.revpiinfo_ui import Ui_diag_revpiinfo
class RevPiInfo(QtWidgets.QDialog, Ui_diag_revpiinfo):

View File

@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
"""View log files from Revolution Pi."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from enum import IntEnum
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
import proginit as pi
from ui.revpilogfile_ui import Ui_win_revpilogfile
from . import helper
from . import proginit as pi
from .ui.revpilogfile_ui import Ui_win_revpilogfile
class LogType(IntEnum):
@@ -20,18 +20,17 @@ class LogType(IntEnum):
class DataThread(QtCore.QThread):
error_detected = QtCore.pyqtSignal(str)
line_logged = QtCore.pyqtSignal(LogType, bool, str)
"""log_type, success, text"""
def __init__(self, parent=None, cycle_time=1000):
super(DataThread, self).__init__(parent)
self._cli = helper.cm.get_cli()
self._cycle_time = cycle_time
self._paused = True
self.error_count = 0
self.max_block = 256000
self.max_block = 16384 # 16 kByte
self.mrk_app = 0
self.mrk_daemon = 0
@@ -45,7 +44,12 @@ class DataThread(QtCore.QThread):
:return: tuple(position: int, EOF: bool)
"""
# Load max data from start position
buff_log = xmlcall(start_position, self.max_block).data
buff_log = helper.cm.call_remote_function(
xmlcall,
start_position,
self.max_block,
raise_exception=True
).data # type: bytes
eof = True
if buff_log == b'\x16': # 'ESC'
@@ -56,20 +60,23 @@ class DataThread(QtCore.QThread):
# The log file was rotated by log rotate on the Revolution Pi
start_position = 0
eof = False
pi.logger.info("RevPi started a new log file.")
elif buff_log:
start_position += len(buff_log)
eof = len(buff_log) < self.max_block
self.line_logged.emit(log_type, True, buff_log.decode("utf-8"))
self.line_logged.emit(log_type, True, buff_log.decode("utf-8", errors="replace"))
return start_position, eof
def pause(self):
"""Stop checking new log lines, but leave thread alive."""
pi.logger.debug("DataThread.pause")
self._paused = True
def resume(self):
"""Start checking for new log lines."""
pi.logger.debug("DataThread.resume")
self._paused = False
def run(self) -> None:
@@ -83,13 +90,13 @@ class DataThread(QtCore.QThread):
while not (eof_app or self.isInterruptionRequested()):
self.mrk_app, eof_app = self._load_log(
LogType.APP,
self._cli.load_applog,
"load_applog",
self.mrk_app,
)
while not (eof_daemon or self.isInterruptionRequested()):
self.mrk_daemon, eof_daemon = self._load_log(
LogType.DAEMON,
self._cli.load_plclog,
"load_plclog",
self.mrk_daemon,
)
self.error_count = 0
@@ -109,7 +116,7 @@ class RevPiLogfile(QtWidgets.QMainWindow, Ui_win_revpilogfile):
super(RevPiLogfile, self).__init__(parent)
self.setupUi(self)
self.th_data = DataThread()
self.th_data = DataThread(self)
self.err_daemon = 0
helper.cm.connection_established.connect(self.on_cm_connection_established)
@@ -120,7 +127,7 @@ class RevPiLogfile(QtWidgets.QMainWindow, Ui_win_revpilogfile):
def _create_data_thread(self):
self.th_data.deleteLater()
self.th_data = DataThread()
self.th_data = DataThread(self)
self.th_data.error_detected.connect(self.txt_daemon.setPlainText)
self.th_data.line_logged.connect(self.on_line_logged)
self.th_data.start()
@@ -159,12 +166,12 @@ class RevPiLogfile(QtWidgets.QMainWindow, Ui_win_revpilogfile):
self.th_data.resume()
@QtCore.pyqtSlot()
def on_btn_daemon_pressed(self):
def on_btn_daemon_clicked(self):
"""Clear the daemon log view."""
self.txt_daemon.clear()
@QtCore.pyqtSlot()
def on_btn_app_pressed(self):
def on_btn_app_clicked(self):
"""Clear the app log view."""
self.txt_app.clear()

View File

@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
"""RevPiPyLoad options window."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
import proginit as pi
from aclmanager import AclManager
from mqttmanager import MqttManager
from ui.revpioption_ui import Ui_diag_options
from . import helper
from . import proginit as pi
from .aclmanager import AclManager
from .mqttmanager import MqttManager
from .ui.revpioption_ui import Ui_diag_options
class RevPiOption(QtWidgets.QDialog, Ui_diag_options):

View File

@@ -1,17 +1,19 @@
# -*- coding: utf-8 -*-
"""Saved connections of Revolution Pi devices."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from enum import IntEnum
import keyring
from PyQt5 import QtCore, QtGui, QtWidgets
from keyring.errors import KeyringError
import helper
import proginit as pi
from helper import WidgetData
from ui.revpiplclist_ui import Ui_diag_connections
from . import helper
from . import proginit as pi
from .helper import RevPiSettings, WidgetData
from .ui.revpiplclist_ui import Ui_diag_connections
class NodeType(IntEnum):
@@ -25,12 +27,11 @@ 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
self.__current_item = QtWidgets.QTreeWidgetItem() # type: QtWidgets.QTreeWidgetItem
self.changes = True
self._keyring_cleanup_id_user = []
self.tre_connections.setColumnWidth(0, 250)
self.lbl_port.setText(self.lbl_port.text().format(self.__default_port))
@@ -43,28 +44,22 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
self.tre_connections.clear()
self.cbb_folder.clear()
self.cbb_folder.addItem("")
for i in range(helper.settings.beginReadArray("connections")):
helper.settings.setArrayIndex(i)
# Get length of array and close it, the RevPiSettings-class need it
count_settings = helper.settings.beginReadArray("connections")
helper.settings.endArray()
for i in range(count_settings):
settings = RevPiSettings(i)
con_item = QtWidgets.QTreeWidgetItem(NodeType.CON)
con_item.setIcon(0, QtGui.QIcon(":/main/ico/cpu.ico"))
con_item.setText(0, helper.settings.value("name", "Revolution Pi", str))
con_item.setText(1, helper.settings.value("address", "127.0.0.1", str))
con_item.setData(0, WidgetData.port, helper.settings.value("port", self.__default_port, int))
con_item.setText(0, settings.name)
con_item.setText(1, settings.address)
con_item.setData(0, WidgetData.last_dir_upload, helper.settings.value("last_dir_upload"))
con_item.setData(0, WidgetData.last_file_upload, helper.settings.value("last_file_upload"))
con_item.setData(0, WidgetData.last_dir_pictory, helper.settings.value("last_dir_pictory"))
con_item.setData(0, WidgetData.last_dir_picontrol, helper.settings.value("last_dir_picontrol"))
con_item.setData(0, WidgetData.last_dir_selected, helper.settings.value("last_dir_selected"))
con_item.setData(0, WidgetData.last_pictory_file, helper.settings.value("last_pictory_file"))
con_item.setData(0, WidgetData.last_tar_file, helper.settings.value("last_tar_file"))
con_item.setData(0, WidgetData.last_zip_file, helper.settings.value("last_zip_file"))
con_item.setData(0, WidgetData.watch_files, helper.settings.value("watch_files"))
con_item.setData(0, WidgetData.watch_path, helper.settings.value("watch_path"))
con_item.setData(0, WidgetData.debug_geos, helper.settings.value("debug_geos"))
con_item.setData(0, WidgetData.revpi_settings, settings)
folder = helper.settings.value("folder", "", str)
folder = settings.folder
if folder:
sub_folder = self._get_folder_item(folder)
if sub_folder is None:
@@ -78,10 +73,8 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
else:
self.tre_connections.addTopLevelItem(con_item)
helper.settings.endArray()
self.tre_connections.expandAll()
self.changes = True
self.changes = False
if self.tre_connections.topLevelItemCount() == 0:
self._edit_state()
@@ -89,53 +82,31 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
def accept(self) -> None:
pi.logger.debug("RevPiPlcList.accept")
def set_settings(node: QtWidgets.QTreeWidgetItem):
parent = node.parent()
helper.settings.setValue("address", node.text(1))
helper.settings.setValue("folder", parent.text(0) if parent else "")
helper.settings.setValue("name", node.text(0))
helper.settings.setValue("port", node.data(0, WidgetData.port))
if node.data(0, WidgetData.last_dir_upload):
helper.settings.setValue("last_dir_upload", node.data(0, WidgetData.last_dir_upload))
if node.data(0, WidgetData.last_file_upload):
helper.settings.setValue("last_file_upload", node.data(0, WidgetData.last_file_upload))
if node.data(0, WidgetData.last_dir_pictory):
helper.settings.setValue("last_dir_pictory", node.data(0, WidgetData.last_dir_pictory))
if node.data(0, WidgetData.last_dir_picontrol):
helper.settings.setValue("last_dir_picontrol", node.data(0, WidgetData.last_dir_picontrol))
if node.data(0, WidgetData.last_dir_selected):
helper.settings.setValue("last_dir_selected", node.data(0, WidgetData.last_dir_selected))
if node.data(0, WidgetData.last_pictory_file):
helper.settings.setValue("last_pictory_file", node.data(0, WidgetData.last_pictory_file))
if node.data(0, WidgetData.last_tar_file):
helper.settings.setValue("last_tar_file", node.data(0, WidgetData.last_tar_file))
if node.data(0, WidgetData.last_zip_file):
helper.settings.setValue("last_zip_file", node.data(0, WidgetData.last_zip_file))
if node.data(0, WidgetData.watch_files):
helper.settings.setValue("watch_files", node.data(0, WidgetData.watch_files))
if node.data(0, WidgetData.watch_path):
helper.settings.setValue("watch_path", node.data(0, WidgetData.watch_path))
if node.data(0, WidgetData.debug_geos):
helper.settings.setValue("debug_geos", node.data(0, WidgetData.debug_geos))
for internal_id, ssh_user in self._keyring_cleanup_id_user:
service_name = "{0}.{1}_{2}".format(
helper.settings.applicationName(),
helper.settings.organizationName(),
internal_id
)
try:
# Remove information from os keyring, which we collected on_btn_delete_clicked
keyring.delete_password(service_name, ssh_user)
except KeyringError as e:
pi.logger.error(e)
helper.settings.remove("connections")
helper.settings.beginWriteArray("connections")
counter_index = 0
for i in range(self.tre_connections.topLevelItemCount()):
root_item = self.tre_connections.topLevelItem(i)
if root_item.type() == NodeType.DIR:
for k in range(root_item.childCount()):
helper.settings.setArrayIndex(counter_index)
set_settings(root_item.child(k))
counter_index += 1
revpi_settings = root_item.child(k).data(0, WidgetData.revpi_settings) # type: RevPiSettings
revpi_settings.folder = root_item.text(0)
revpi_settings.save_settings()
elif root_item.type() == NodeType.CON:
helper.settings.setArrayIndex(counter_index)
set_settings(root_item)
counter_index += 1
helper.settings.endArray()
revpi_settings = root_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
revpi_settings.folder = ""
revpi_settings.save_settings()
self.changes = False
super(RevPiPlcList, self).accept()
@@ -158,6 +129,17 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
self._load_settings()
return super(RevPiPlcList, self).exec()
def exec_with_presets(self, presets: RevPiSettings) -> int:
"""
Start dialog with new created settings object and presets.
:param presets: Use these settings as preset
:return: Dialog status
"""
self._load_settings()
self.on_btn_add_clicked(presets)
return super(RevPiPlcList, self).exec()
@QtCore.pyqtSlot(QtWidgets.QAbstractButton)
def on_btn_box_clicked(self, button: QtWidgets.QAbstractButton):
if self.btn_box.buttonRole(button) == QtWidgets.QDialogButtonBox.DestructiveRole:
@@ -167,6 +149,7 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
# region # REGION: Connection management
def _edit_state(self):
"""Set enabled status of all controls, depending on selected item."""
item = self.tre_connections.currentItem()
if item is None:
up_ok = False
@@ -192,8 +175,13 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
self.txt_name.setEnabled(con_item)
self.txt_address.setEnabled(con_item)
self.sbx_port.setEnabled(con_item)
self.sbx_timeout.setEnabled(con_item)
self.cbb_folder.setEnabled(con_item or dir_item)
self.cbx_ssh_use_tunnel.setEnabled(con_item)
self.sbx_ssh_port.setEnabled(con_item)
self.txt_ssh_user.setEnabled(con_item)
def _get_folder_item(self, name: str):
"""Find the folder entry by name."""
for i in range(self.tre_connections.topLevelItemCount()):
@@ -215,6 +203,7 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
if 0 <= new_index < dir_item.childCount():
item = dir_item.takeChild(index)
dir_item.insertChild(new_index, item)
self.tre_connections.expandItem(dir_item)
else:
index = self.tre_connections.indexOfTopLevelItem(item)
new_index = index + count
@@ -232,33 +221,48 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
self._edit_state()
if current and current.type() == NodeType.CON:
self.__current_item = current
self.txt_name.setText(current.text(0))
self.txt_address.setText(current.text(1))
self.sbx_port.setValue(current.data(0, WidgetData.port))
settings = current.data(0, WidgetData.revpi_settings) # type: RevPiSettings
self.txt_name.setText(settings.name)
self.txt_address.setText(settings.address)
self.sbx_port.setValue(settings.port)
self.sbx_timeout.setValue(settings.timeout)
if current.parent() is None:
self.cbb_folder.setCurrentIndex(0)
else:
self.cbb_folder.setCurrentText(current.parent().text(0))
self.cbx_ssh_use_tunnel.setChecked(settings.ssh_use_tunnel)
self.sbx_ssh_port.setValue(settings.ssh_port)
self.txt_ssh_user.setText(settings.ssh_user)
elif current and current.type() == NodeType.DIR:
self.__current_item = current
self.cbb_folder.setCurrentText(current.text(0))
else:
self.__current_item = QtWidgets.QTreeWidgetItem()
self.cbb_folder.setCurrentText(current.text(0) if current else "")
@QtCore.pyqtSlot()
def on_btn_up_pressed(self):
def on_btn_up_clicked(self):
self._move_item(-1)
@QtCore.pyqtSlot()
def on_btn_down_pressed(self):
def on_btn_down_clicked(self):
self._move_item(1)
@QtCore.pyqtSlot()
def on_btn_delete_pressed(self):
def on_btn_delete_clicked(self):
"""Remove selected entry."""
item = self.tre_connections.currentItem()
if item and item.type() == NodeType.CON:
revpi_settings = item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
if revpi_settings.ssh_saved_password:
# Cleans up keyring in save function
self._keyring_cleanup_id_user.append((revpi_settings.internal_id, revpi_settings.ssh_user))
dir_node = item.parent()
if dir_node:
dir_node.removeChild(item)
@@ -269,19 +273,21 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
self._edit_state()
@QtCore.pyqtSlot()
def on_btn_add_pressed(self):
def on_btn_add_clicked(self, settings_preset: RevPiSettings = None):
"""Create new element."""
self.__current_item = QtWidgets.QTreeWidgetItem(NodeType.CON)
self.__current_item.setIcon(0, QtGui.QIcon(":/main/ico/cpu.ico"))
self.__current_item.setText(0, self.__default_name)
self.__current_item.setData(0, WidgetData.port, self.__default_port)
settings = settings_preset or RevPiSettings()
new_item = QtWidgets.QTreeWidgetItem(NodeType.CON)
new_item.setIcon(0, QtGui.QIcon(":/main/ico/cpu.ico"))
new_item.setText(0, settings.name)
new_item.setData(0, WidgetData.revpi_settings, settings)
sub_folder = self._get_folder_item(self.cbb_folder.currentText())
if sub_folder:
sub_folder.addChild(self.__current_item)
sub_folder.addChild(new_item)
else:
self.tre_connections.addTopLevelItem(self.__current_item)
self.tre_connections.addTopLevelItem(new_item)
self.tre_connections.setCurrentItem(self.__current_item)
# This will load all settings and prepare widgets
self.tre_connections.setCurrentItem(new_item)
self.txt_name.setFocus()
self.txt_name.selectAll()
@@ -290,18 +296,58 @@ class RevPiPlcList(QtWidgets.QDialog, Ui_diag_connections):
if self.__current_item.type() != NodeType.CON:
return
self.__current_item.setText(0, text)
settings = self.__current_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
settings.name = text
self.changes = True
@QtCore.pyqtSlot(str)
def on_txt_address_textEdited(self, text):
if self.__current_item.type() != NodeType.CON:
return
self.__current_item.setText(1, text)
settings = self.__current_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
settings.address = text
self.changes = True
@QtCore.pyqtSlot(int)
def on_sbx_port_valueChanged(self, value: int):
if self.__current_item.type() != NodeType.CON:
return
self.__current_item.setData(0, WidgetData.port, value)
settings = self.__current_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
settings.port = value
self.changes = True
@QtCore.pyqtSlot(int)
def on_sbx_timeout_valueChanged(self, value: int):
if self.__current_item.type() != NodeType.CON:
return
settings = self.__current_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
settings.timeout = value
self.changes = True
@QtCore.pyqtSlot(int)
def on_cbx_ssh_use_tunnel_stateChanged(self, check_state: int):
if self.__current_item.type() != NodeType.CON:
return
settings = self.__current_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
settings.ssh_use_tunnel = check_state == QtCore.Qt.CheckState.Checked
self.changes = True
@QtCore.pyqtSlot(int)
def on_sbx_ssh_port_valueChanged(self, value: int):
if self.__current_item.type() != NodeType.CON:
return
settings = self.__current_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
settings.ssh_port = value
self.changes = True
@QtCore.pyqtSlot(str)
def on_txt_ssh_user_textEdited(self, text):
if self.__current_item.type() != NodeType.CON:
return
settings = self.__current_item.data(0, WidgetData.revpi_settings) # type: RevPiSettings
settings.ssh_user = text
self.changes = True
@QtCore.pyqtSlot(str)
def on_cbb_folder_editTextChanged(self, text: str):

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Revolution Pi PLC program configuration."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018 Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
import gzip
@@ -9,14 +9,14 @@ import os
import tarfile
import zipfile
from shutil import rmtree
from tempfile import mkdtemp, mkstemp
from tempfile import mkdtemp
from xmlrpc.client import Binary
from PyQt5 import QtCore, QtGui, QtWidgets
import helper
import proginit as pi
from ui.revpiprogram_ui import Ui_diag_program
from . import helper
from . import proginit as pi
from .ui.revpiprogram_ui import Ui_diag_program
class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
@@ -58,8 +58,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
:return: True, if unsaved changes was found
"""
return \
self.cbb_plcprogram.currentText() != self.dc.get("plcprogram", "") or \
return self.cbb_plcprogram.currentText() != self.dc.get("plcprogram", "") or \
self.txt_plcarguments.text() != self.dc.get("plcarguments", "") or \
self.rbn_pythonversion_2.isChecked() != (self.dc.get("pythonversion", 3) == 2) or \
self.rbn_pythonversion_3.isChecked() != (self.dc.get("pythonversion", 3) == 3) or \
@@ -174,7 +173,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
self._load_settings()
self._apply_acl()
return super(RevPiProgram, self).exec()
def reject(self) -> None:
@@ -213,7 +212,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
)
)
elif ec == 0:
helper.cm.program_last_pictory_file = filename
helper.cm.settings.last_pictory_file = filename
if ask == QtWidgets.QMessageBox.Yes:
QtWidgets.QMessageBox.information(
self, self.tr("Success"), self.tr(
@@ -319,21 +318,16 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
self.cbx_pictory.setEnabled(index >= 1)
@QtCore.pyqtSlot()
def on_btn_program_download_pressed(self):
def on_btn_program_download_clicked(self):
"""Download plc program from Revolution Pi."""
if not helper.cm.connected:
return
selected_dir = ""
if self.cbb_format.currentIndex() == 0:
# Save files as zip archive
diag_save = QtWidgets.QFileDialog(
self, self.tr("Save ZIP archive..."),
os.path.join(
helper.cm.program_last_zip_file,
"{0}.zip".format(helper.cm.name)
),
helper.cm.settings.last_zip_file or "{0}.zip".format(helper.cm.settings.name),
self.tr("ZIP archive (*.zip);;All files (*.*)")
)
diag_save.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
@@ -345,16 +339,13 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
filename = diag_save.selectedFiles()[0]
fh = open(filename, "wb")
helper.cm.program_last_zip_file = filename
helper.cm.settings.last_zip_file = filename
elif self.cbb_format.currentIndex() == 1:
# Save files as TarGz archive
diag_save = QtWidgets.QFileDialog(
self, self.tr("Save TGZ archive..."),
os.path.join(
helper.cm.program_last_tar_file,
"{0}.tgz".format(helper.cm.name)
),
helper.cm.settings.last_tar_file or "{0}.tgz".format(helper.cm.settings.name),
self.tr("TGZ archive (*.tgz);;All files (*.*)")
)
diag_save.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
@@ -366,7 +357,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
filename = diag_save.selectedFiles()[0]
fh = open(filename, "wb")
helper.cm.program_last_tar_file = filename
helper.cm.settings.last_tar_file = filename
else:
# Other indexes are not allowed for download
@@ -405,7 +396,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
)
@QtCore.pyqtSlot()
def on_btn_program_upload_pressed(self):
def on_btn_program_upload_clicked(self):
"""Upload plc program to Revolution Pi."""
if not helper.cm.connected:
return
@@ -426,7 +417,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
# Upload zip archive content
diag_open = QtWidgets.QFileDialog(
self, self.tr("Upload content of ZIP archive..."),
helper.cm.program_last_file_upload,
helper.cm.settings.last_file_upload,
self.tr("ZIP archive (*.zip);;All files (*.*)")
)
diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
@@ -438,7 +429,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
return
filename = diag_open.selectedFiles()[0]
helper.cm.program_last_file_upload = filename
helper.cm.settings.last_file_upload = filename
if zipfile.is_zipfile(filename):
dirtmp = mkdtemp()
fhz = zipfile.ZipFile(filename)
@@ -460,7 +451,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
# Upload TarGz content
diag_open = QtWidgets.QFileDialog(
self, self.tr("Upload content of TAR archive..."),
helper.cm.program_last_file_upload,
helper.cm.settings.last_file_upload,
self.tr("TAR archive (*.tgz);;All files (*.*)")
)
diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
@@ -472,7 +463,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
return
filename = diag_open.selectedFiles()[0]
helper.cm.program_last_file_upload = filename
helper.cm.settings.last_file_upload = filename
if tarfile.is_tarfile(filename):
dirtmp = mkdtemp()
fht = tarfile.open(filename)
@@ -598,7 +589,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
# region # REGION: Control files
@QtCore.pyqtSlot()
def on_btn_pictory_download_pressed(self):
def on_btn_pictory_download_clicked(self):
"""Download piCtory configuration."""
if not helper.cm.connected:
return
@@ -606,8 +597,8 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
diag_save = QtWidgets.QFileDialog(
self, self.tr("Save piCtory file..."),
os.path.join(
helper.cm.program_last_dir_pictory,
"{0}.rsc".format(helper.cm.name)
helper.cm.settings.last_dir_pictory,
"{0}.rsc".format(helper.cm.settings.name)
),
self.tr("piCtory file (*.rsc);;All files (*.*)")
)
@@ -619,7 +610,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
return
filename = diag_save.selectedFiles()[0]
helper.cm.program_last_dir_pictory = os.path.dirname(filename)
helper.cm.settings.last_dir_pictory = os.path.dirname(filename)
bin_buffer = helper.cm.call_remote_function("get_pictoryrsc") # type: Binary
if bin_buffer is None:
QtWidgets.QMessageBox.critical(
@@ -635,18 +626,17 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
QtWidgets.QMessageBox.information(
self, self.tr("Success"), self.tr(
"piCtory configuration successfully loaded and saved to:\n{0}."
"".format(filename)
)
).format(filename)
)
@QtCore.pyqtSlot()
def on_btn_pictory_upload_pressed(self):
def on_btn_pictory_upload_clicked(self):
if not helper.cm.connected:
return
diag_open = QtWidgets.QFileDialog(
self, self.tr("Upload piCtory file..."),
helper.cm.program_last_pictory_file,
helper.cm.settings.last_pictory_file or "{0}.rsc".format(helper.cm.settings.name),
self.tr("piCtory file (*.rsc);;All files (*.*)")
)
diag_open.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
@@ -660,7 +650,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
self._upload_pictory(diag_open.selectedFiles()[0])
@QtCore.pyqtSlot()
def on_btn_procimg_download_pressed(self):
def on_btn_procimg_download_clicked(self):
"""Download process image."""
if not helper.cm.connected:
return
@@ -669,8 +659,8 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
self,
self.tr("Save piControl file..."),
os.path.join(
helper.cm.program_last_dir_picontrol,
"{0}.img".format(helper.cm.name)
helper.cm.settings.last_dir_picontrol,
"{0}.img".format(helper.cm.settings.name)
),
self.tr("Process image file (*.img);;All files (*.*)")
)
@@ -682,7 +672,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
return
filename = diag_save.selectedFiles()[0]
helper.cm.program_last_dir_picontrol = os.path.dirname(filename)
helper.cm.settings.last_dir_picontrol = os.path.dirname(filename)
bin_buffer = helper.cm.call_remote_function("get_procimg") # type: Binary
if bin_buffer is None:
@@ -699,8 +689,7 @@ class RevPiProgram(QtWidgets.QDialog, Ui_diag_program):
QtWidgets.QMessageBox.information(
self, self.tr("Success"), self.tr(
"Process image successfully loaded and saved to:\n{0}."
"".format(filename)
)
).format(filename)
)
# endregion # # # # #

View File

@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""Simulator for piControl."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from os import W_OK, access
from os.path import basename, dirname, exists, join
from PyQt5 import QtCore, QtGui, QtWidgets
from . 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) if exists(procimg_file) else access(dirname(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)

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""Package of ssh tunnel connections."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"

View File

@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
"""
Connect to a remote host and tunnel a port.
This was crated on base of the paramiko library demo file forward.py, see on
GitHub https://github.com/paramiko/paramiko/blob/main/demos/forward.py
"""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
import select
from socketserver import BaseRequestHandler, ThreadingTCPServer
from threading import Thread
from paramiko.client import SSHClient, WarningPolicy
from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import PasswordRequiredException
from paramiko.transport import Transport
class ForwardServer(ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
class Handler(BaseRequestHandler):
def handle(self):
try:
chan = self.ssh_transport.open_channel(
"direct-tcpip",
(self.chain_host, self.chain_port),
self.request.getpeername(),
)
except Exception as e:
return
if chan is None:
return
while True:
r, w, x = select.select([self.request, chan], [], [], 5.0)
if self.request in r:
data = self.request.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
self.request.send(data)
chan.close()
self.request.close()
class SSHLocalTunnel:
def __init__(self, remote_tunnel_port: int, ssh_host: str, ssh_port: int = 22):
"""
Connect to a ssh remote host and tunnel a port to your host.
:param remote_tunnel_port: Port on the remote host to tunnel through ssh
:param ssh_host: ssh remote host address
:param ssh_port: ssh remote host port
"""
self._remote_tunnel_port = remote_tunnel_port
self._ssh_host = ssh_host
self._ssh_port = ssh_port
self._th_server = Thread()
self._ssh_client = SSHClient()
self._ssh_client.load_system_host_keys()
self._ssh_client.set_missing_host_key_policy(WarningPolicy())
self._ssh_transport = None # type: Transport
self._forward_server = None # type: ThreadingTCPServer
self._local_tunnel_port = None # type: int
def __th_target(self):
"""Server thread for socket mirror."""
self._forward_server.serve_forever()
def _configure_forward_server(self) -> int:
"""
Configure forward server for port mirror.
:return: Local port on wich the remote port is connected
"""
self._ssh_transport = self._ssh_client.get_transport()
class SubHandler(Handler):
chain_host = "127.0.0.1"
chain_port = self._remote_tunnel_port
ssh_transport = self._ssh_transport
self._forward_server = ForwardServer(("127.0.0.1", 0), SubHandler)
self._local_tunnel_port = self._forward_server.socket.getsockname()[1]
self._th_server = Thread(target=self.__th_target)
self._th_server.start()
return self._local_tunnel_port
def connect_by_credentials(self, username: str, password: str) -> int:
"""
Connect to a ssh remote host and tunnel specified port of localhost.
:return: Local port on wich the remote port is connected
"""
if self._th_server.is_alive():
raise RuntimeError("Already connected")
self._ssh_client.connect(
hostname=self._ssh_host,
port=self._ssh_port,
username=username,
password=password,
)
return self._configure_forward_server()
def connect_by_keyfile(self, username: str, key_file: str, key_password: str = None) -> int:
"""
Connect to a ssh remote host and tunnel specified port of localhost.
:return: Local port on wich the remote port is connected
"""
if self._th_server.is_alive():
raise RuntimeError("Already connected")
if self.key_file_password_protected(key_file):
private_key = RSAKey.from_private_key_file(key_file, key_password)
else:
private_key = RSAKey.from_private_key_file(key_file)
self._ssh_client.connect(
hostname=self._ssh_host,
port=self._ssh_port,
username=username,
pkey=private_key,
look_for_keys=True,
)
return self._configure_forward_server()
def disconnect(self):
"""Close SSH tunnel connection."""
self._local_tunnel_port = None
if self._forward_server:
self._forward_server.shutdown()
self._forward_server.server_close()
if self._ssh_transport:
self._ssh_transport.close()
self._ssh_client.close()
@staticmethod
def key_file_password_protected(key_file: str) -> bool:
try:
RSAKey.from_private_key_file(key_file)
except PasswordRequiredException:
return True
return False
@property
def connected(self):
"""Check connection state of ssh tunnel."""
return self._ssh_transport and self._ssh_transport.is_active()
@property
def local_tunnel_port(self) -> int:
return self._local_tunnel_port

View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
"""Authentication dialog for SSH."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"
from logging import getLogger
import keyring
from PyQt5 import QtWidgets
from keyring.errors import KeyringError
from .ui.sshauth_ui import Ui_diag_sshauth
log = getLogger()
class SSHAuth(QtWidgets.QDialog, Ui_diag_sshauth):
def __init__(self, user_name="", service_name: str = None, parent=None):
"""
Ask the user for username and password or use saved entries.
If you want to use the operating system's password storage, you have
to set a 'service_name'. The value must be unique for your application
or for each user, if the username is the same.
:param user_name: Preset username, also used to check password save
:param service_name: Identity to save passwords in os's password save
:param parent: Qt parent for this dialog
"""
log.debug("SSHAuth.__init__")
super(SSHAuth, self).__init__(parent)
self.setupUi(self)
self._in_keyring = False
self._service_name = service_name
self.cbx_save_password.setVisible(bool(service_name))
self.txt_username.setText(user_name)
def accept(self) -> None:
log.debug("SSHAuth.accept")
if self._service_name and self.cbx_save_password.isChecked():
try:
keyring.set_password(self._service_name, self.username, self.password)
except KeyringError as e:
log.error(e)
self._in_keyring = False
QtWidgets.QMessageBox.warning(
self, self.tr("Could not save password"), self.tr(
"Could not save password to operating systems password save.\n\n"
"Maybe your operating system does not support saving passwords. "
"This could be due to missing libraries or programs.\n\n"
"This is not an error of RevPi Commander."
)
)
else:
self._in_keyring = True
super().accept()
def exec(self) -> int:
log.debug("SSHAuth.exec")
if self._service_name:
try:
saved_password = keyring.get_password(self._service_name, self.username)
except KeyringError as e:
log.error(e)
self._in_keyring = False
else:
if saved_password:
self._in_keyring = True
self.txt_password.setText(saved_password)
return QtWidgets.QDialog.Accepted
return super().exec()
def remove_saved_password(self) -> None:
"""Remove saved password."""
log.debug("SSHAuth.remove_saved_password")
if self._service_name:
try:
keyring.delete_password(self._service_name, self.username)
except KeyringError as e:
log.error(e)
@property
def in_keyring(self) -> bool:
"""True, if password is in keyring."""
return self._in_keyring
@property
def password(self) -> str:
"""Get the saved or entered password."""
return self.txt_password.text()
@property
def username(self) -> str:
"""Get the entered username."""
return self.txt_username.text()

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""Package of compiled Qt ui files."""
__author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv3"

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'aclmanager.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -87,6 +87,7 @@ class Ui_diag_aclmanager(object):
self.txt_ip_a.setObjectName("txt_ip_a")
self.hl_ip.addWidget(self.txt_ip_a)
self.lbl_ip_a = QtWidgets.QLabel(self.gb_edit)
self.lbl_ip_a.setText(".")
self.lbl_ip_a.setObjectName("lbl_ip_a")
self.hl_ip.addWidget(self.lbl_ip_a)
self.txt_ip_b = QtWidgets.QLineEdit(self.gb_edit)
@@ -94,6 +95,7 @@ class Ui_diag_aclmanager(object):
self.txt_ip_b.setObjectName("txt_ip_b")
self.hl_ip.addWidget(self.txt_ip_b)
self.lbl_ip_b = QtWidgets.QLabel(self.gb_edit)
self.lbl_ip_b.setText(".")
self.lbl_ip_b.setObjectName("lbl_ip_b")
self.hl_ip.addWidget(self.lbl_ip_b)
self.txt_ip_c = QtWidgets.QLineEdit(self.gb_edit)
@@ -101,6 +103,7 @@ class Ui_diag_aclmanager(object):
self.txt_ip_c.setObjectName("txt_ip_c")
self.hl_ip.addWidget(self.txt_ip_c)
self.lbl_ip_c = QtWidgets.QLabel(self.gb_edit)
self.lbl_ip_c.setText(".")
self.lbl_ip_c.setObjectName("lbl_ip_c")
self.hl_ip.addWidget(self.lbl_ip_c)
self.txt_ip_d = QtWidgets.QLineEdit(self.gb_edit)
@@ -121,8 +124,8 @@ class Ui_diag_aclmanager(object):
self.verticalLayout.addWidget(self.btn_box)
self.retranslateUi(diag_aclmanager)
self.btn_box.accepted.connect(diag_aclmanager.accept)
self.btn_box.rejected.connect(diag_aclmanager.reject)
self.btn_box.accepted.connect(diag_aclmanager.accept) # type: ignore
self.btn_box.rejected.connect(diag_aclmanager.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_aclmanager)
diag_aclmanager.setTabOrder(self.tb_acls, self.btn_edit)
diag_aclmanager.setTabOrder(self.btn_edit, self.btn_remove)
@@ -147,9 +150,6 @@ class Ui_diag_aclmanager(object):
self.btn_add.setText(_translate("diag_aclmanager", "&Save entry"))
self.lbl_ip.setText(_translate("diag_aclmanager", "IP address:"))
self.lbl_level.setText(_translate("diag_aclmanager", "Access level:"))
self.lbl_ip_a.setText(_translate("diag_aclmanager", "."))
self.lbl_ip_b.setText(_translate("diag_aclmanager", "."))
self.lbl_ip_c.setText(_translate("diag_aclmanager", "."))
if __name__ == "__main__":

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'avahisearch.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -65,9 +65,21 @@ class Ui_diag_search(object):
self.btn_box.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
self.btn_box.setObjectName("btn_box")
self.gridLayout.addWidget(self.btn_box, 3, 0, 1, 2)
self.act_copy_host = QtWidgets.QAction(diag_search)
self.act_copy_host.setObjectName("act_copy_host")
self.act_copy_ip = QtWidgets.QAction(diag_search)
self.act_copy_ip.setObjectName("act_copy_ip")
self.act_open_pictory = QtWidgets.QAction(diag_search)
self.act_open_pictory.setObjectName("act_open_pictory")
self.act_connect_ssh = QtWidgets.QAction(diag_search)
self.act_connect_ssh.setObjectName("act_connect_ssh")
self.act_connect_xmlrpc = QtWidgets.QAction(diag_search)
self.act_connect_xmlrpc.setObjectName("act_connect_xmlrpc")
self.act_connect = QtWidgets.QAction(diag_search)
self.act_connect.setObjectName("act_connect")
self.retranslateUi(diag_search)
self.btn_box.rejected.connect(diag_search.reject)
self.btn_box.rejected.connect(diag_search.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_search)
def retranslateUi(self, diag_search):
@@ -82,6 +94,15 @@ class Ui_diag_search(object):
item.setText(_translate("diag_search", "IP address"))
self.btn_connect.setText(_translate("diag_search", "&Connect to Revolution Pi"))
self.btn_save.setText(_translate("diag_search", "&Save connection"))
self.act_copy_host.setText(_translate("diag_search", "Copy host name"))
self.act_copy_ip.setText(_translate("diag_search", "Copy IP address"))
self.act_open_pictory.setText(_translate("diag_search", "Open piCtory"))
self.act_connect_ssh.setText(_translate("diag_search", "Connect via SSH (recommended)"))
self.act_connect_ssh.setToolTip(_translate("diag_search", "Establish a connection via encrypted SSH tunnel"))
self.act_connect_xmlrpc.setText(_translate("diag_search", "Connect via XML-RPC"))
self.act_connect_xmlrpc.setToolTip(_translate("diag_search", "You have to configure your Revolution Pi to accept this connections"))
self.act_connect.setText(_translate("diag_search", "Connect"))
self.act_connect.setToolTip(_translate("diag_search", "Connect to Revoluton Pi"))
from . import ressources_rc

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'backgroundworker.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
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_())

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'debugcontrol.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -36,9 +36,15 @@ class Ui_wid_debugcontrol(object):
self.verticalLayout = QtWidgets.QVBoxLayout(self.gb_control)
self.verticalLayout.setObjectName("verticalLayout")
self.btn_read_io = QtWidgets.QPushButton(self.gb_control)
self.btn_read_io.setAutoRepeat(True)
self.btn_read_io.setAutoRepeatDelay(100)
self.btn_read_io.setAutoRepeatInterval(200)
self.btn_read_io.setObjectName("btn_read_io")
self.verticalLayout.addWidget(self.btn_read_io)
self.btn_refresh_io = QtWidgets.QPushButton(self.gb_control)
self.btn_refresh_io.setAutoRepeat(True)
self.btn_refresh_io.setAutoRepeatDelay(100)
self.btn_refresh_io.setAutoRepeatInterval(200)
self.btn_refresh_io.setObjectName("btn_refresh_io")
self.verticalLayout.addWidget(self.btn_refresh_io)
self.btn_write_o = QtWidgets.QPushButton(self.gb_control)
@@ -62,15 +68,16 @@ class Ui_wid_debugcontrol(object):
self.gb_devices.setTitle(_translate("wid_debugcontrol", "Revolution Pi devices"))
self.cbx_stay_on_top.setText(_translate("wid_debugcontrol", "Open to stay on top"))
self.gb_control.setTitle(_translate("wid_debugcontrol", "IO Control"))
self.btn_read_io.setToolTip(_translate("wid_debugcontrol", "Read all IO values and discard local changes (F4)"))
self.btn_read_io.setToolTip(_translate("wid_debugcontrol", "Read all IO values and discard local changes (F4)\n"
"\n"
"Hold this button pressed and it will refresh the IOs every 200 ms."))
self.btn_read_io.setText(_translate("wid_debugcontrol", "Read &all IO values"))
self.btn_read_io.setShortcut(_translate("wid_debugcontrol", "F4"))
self.btn_refresh_io.setToolTip(_translate("wid_debugcontrol", "Refresh all IO values which are locally not changed (F5)"))
self.btn_refresh_io.setToolTip(_translate("wid_debugcontrol", "Refresh all IO values which are locally not changed (F5)\n"
"\n"
"Hold this button pressed and it will refresh the IOs every 200 ms."))
self.btn_refresh_io.setText(_translate("wid_debugcontrol", "&Refresh unchanged IOs"))
self.btn_refresh_io.setShortcut(_translate("wid_debugcontrol", "F5"))
self.btn_write_o.setToolTip(_translate("wid_debugcontrol", "Write locally changed output values to process image (F6)"))
self.btn_write_o.setText(_translate("wid_debugcontrol", "&Write changed outputs"))
self.btn_write_o.setShortcut(_translate("wid_debugcontrol", "F6"))
self.cbx_refresh.setText(_translate("wid_debugcontrol", "&Auto refresh values"))
self.cbx_write.setText(_translate("wid_debugcontrol", "and write outputs"))

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'debugios.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'files.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -56,6 +56,8 @@ class Ui_win_files(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lbl_path_local.sizePolicy().hasHeightForWidth())
self.lbl_path_local.setSizePolicy(sizePolicy)
self.lbl_path_local.setToolTip("/")
self.lbl_path_local.setText("/")
self.lbl_path_local.setObjectName("lbl_path_local")
self.gridLayout_2.addWidget(self.lbl_path_local, 1, 0, 1, 3)
self.gridLayout_2.setColumnStretch(0, 1)
@@ -98,6 +100,8 @@ class Ui_win_files(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lbl_path_revpi.sizePolicy().hasHeightForWidth())
self.lbl_path_revpi.setSizePolicy(sizePolicy)
self.lbl_path_revpi.setToolTip("/")
self.lbl_path_revpi.setText("/")
self.lbl_path_revpi.setObjectName("lbl_path_revpi")
self.gridLayout_3.addWidget(self.lbl_path_revpi, 1, 0, 1, 2)
self.lbl_select_revpi = QtWidgets.QLabel(self.gb_select_revpi)
@@ -161,12 +165,8 @@ class Ui_win_files(object):
self.lbl_select_local.setText(_translate("win_files", "Path to development root:"))
self.btn_select_local.setToolTip(_translate("win_files", "Open developer root directory"))
self.btn_refresh_local.setToolTip(_translate("win_files", "Reload file list"))
self.lbl_path_local.setToolTip(_translate("win_files", "/"))
self.lbl_path_local.setText(_translate("win_files", "/"))
self.tree_files_local.setSortingEnabled(True)
self.gb_select_revpi.setTitle(_translate("win_files", "Revolution Pi"))
self.lbl_path_revpi.setToolTip(_translate("win_files", "/"))
self.lbl_path_revpi.setText(_translate("win_files", "/"))
self.lbl_select_revpi.setText(_translate("win_files", "RevPiPyLoad working directory:"))
self.btn_refresh_revpi.setToolTip(_translate("win_files", "Reload file list"))
self.tree_files_revpi.setSortingEnabled(True)

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'mqttmanager.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -118,8 +118,8 @@ class Ui_diag_mqtt(object):
self.verticalLayout.addWidget(self.btn_box)
self.retranslateUi(diag_mqtt)
self.btn_box.accepted.connect(diag_mqtt.accept)
self.btn_box.rejected.connect(diag_mqtt.reject)
self.btn_box.accepted.connect(diag_mqtt.accept) # type: ignore
self.btn_box.rejected.connect(diag_mqtt.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_mqtt)
def retranslateUi(self, diag_mqtt):

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'revpicommander.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,45 +14,78 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_win_revpicommander(object):
def setupUi(self, win_revpicommander):
win_revpicommander.setObjectName("win_revpicommander")
win_revpicommander.resize(318, 273)
win_revpicommander.resize(353, 299)
win_revpicommander.setWindowTitle("RevPi Commander")
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/main/ico/revpipycontrol.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
win_revpicommander.setWindowIcon(icon)
self.centralwidget = QtWidgets.QWidget(win_revpicommander)
self.centralwidget.setObjectName("centralwidget")
self.gl = QtWidgets.QGridLayout(self.centralwidget)
self.gl = QtWidgets.QVBoxLayout(self.centralwidget)
self.gl.setObjectName("gl")
self.hzl_connection = QtWidgets.QHBoxLayout()
self.hzl_connection.setObjectName("hzl_connection")
self.txt_host = QtWidgets.QLineEdit(self.centralwidget)
self.txt_host.setFocusPolicy(QtCore.Qt.NoFocus)
self.txt_host.setText("")
self.txt_host.setReadOnly(True)
self.txt_host.setObjectName("txt_host")
self.hzl_connection.addWidget(self.txt_host)
self.txt_connection = QtWidgets.QLineEdit(self.centralwidget)
self.txt_connection.setFocusPolicy(QtCore.Qt.NoFocus)
self.txt_connection.setText("")
self.txt_connection.setReadOnly(True)
self.txt_connection.setObjectName("txt_connection")
self.gl.addWidget(self.txt_connection, 0, 0, 1, 1)
self.hzl_connection.addWidget(self.txt_connection)
self.gl.addLayout(self.hzl_connection)
self.btn_plc_start = QtWidgets.QPushButton(self.centralwidget)
icon1 = QtGui.QIcon()
icon1.addPixmap(QtGui.QPixmap(":/action/ico/system-run.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btn_plc_start.setIcon(icon1)
self.btn_plc_start.setObjectName("btn_plc_start")
self.gl.addWidget(self.btn_plc_start, 1, 0, 1, 1)
self.gl.addWidget(self.btn_plc_start)
self.btn_plc_stop = QtWidgets.QPushButton(self.centralwidget)
icon2 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/action/ico/process-stop.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btn_plc_stop.setIcon(icon2)
self.btn_plc_stop.setObjectName("btn_plc_stop")
self.gl.addWidget(self.btn_plc_stop, 2, 0, 1, 1)
self.gl.addWidget(self.btn_plc_stop)
self.btn_plc_restart = QtWidgets.QPushButton(self.centralwidget)
icon3 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/action/ico/view-refresh.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btn_plc_restart.setIcon(icon3)
self.btn_plc_restart.setObjectName("btn_plc_restart")
self.gl.addWidget(self.btn_plc_restart, 3, 0, 1, 1)
self.gl.addWidget(self.btn_plc_restart)
self.btn_plc_logs = QtWidgets.QPushButton(self.centralwidget)
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/action/ico/applications-utilities.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btn_plc_logs.setIcon(icon4)
self.btn_plc_logs.setObjectName("btn_plc_logs")
self.gl.addWidget(self.btn_plc_logs, 4, 0, 1, 1)
self.gl.addWidget(self.btn_plc_logs)
self.hzl_status = QtWidgets.QHBoxLayout()
self.hzl_status.setObjectName("hzl_status")
self.lbl_status = QtWidgets.QLabel(self.centralwidget)
self.lbl_status.setObjectName("lbl_status")
self.hzl_status.addWidget(self.lbl_status)
self.txt_status = QtWidgets.QLineEdit(self.centralwidget)
self.txt_status.setFocusPolicy(QtCore.Qt.NoFocus)
self.txt_status.setText("")
self.txt_status.setAlignment(QtCore.Qt.AlignCenter)
self.txt_status.setReadOnly(True)
self.txt_status.setObjectName("txt_status")
self.gl.addWidget(self.txt_status, 5, 0, 1, 1)
self.hzl_status.addWidget(self.txt_status)
self.gl.addLayout(self.hzl_status)
self.btn_plc_debug = QtWidgets.QPushButton(self.centralwidget)
self.btn_plc_debug.setMinimumSize(QtCore.QSize(300, 0))
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/action/ico/edit-find.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btn_plc_debug.setIcon(icon5)
self.btn_plc_debug.setCheckable(True)
self.btn_plc_debug.setObjectName("btn_plc_debug")
self.gl.addWidget(self.btn_plc_debug, 6, 0, 1, 1)
self.gl.addWidget(self.btn_plc_debug)
win_revpicommander.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(win_revpicommander)
self.menubar.setGeometry(QtCore.QRect(0, 0, 318, 22))
self.menubar.setGeometry(QtCore.QRect(0, 0, 353, 24))
self.menubar.setObjectName("menubar")
self.men_file = QtWidgets.QMenu(self.menubar)
self.men_file.setObjectName("men_file")
@@ -68,8 +101,10 @@ class Ui_win_revpicommander(object):
self.statusbar.setObjectName("statusbar")
win_revpicommander.setStatusBar(self.statusbar)
self.act_connections = QtWidgets.QAction(win_revpicommander)
self.act_connections.setShortcut("Ctrl+N")
self.act_connections.setObjectName("act_connections")
self.act_search = QtWidgets.QAction(win_revpicommander)
self.act_search.setShortcut("Ctrl+F")
self.act_search.setObjectName("act_search")
self.act_quit = QtWidgets.QAction(win_revpicommander)
self.act_quit.setObjectName("act_quit")
@@ -78,16 +113,21 @@ class Ui_win_revpicommander(object):
self.act_info = QtWidgets.QAction(win_revpicommander)
self.act_info.setObjectName("act_info")
self.act_logs = QtWidgets.QAction(win_revpicommander)
self.act_logs.setShortcut("Ctrl+L")
self.act_logs.setObjectName("act_logs")
self.act_options = QtWidgets.QAction(win_revpicommander)
self.act_options.setShortcut("Ctrl+O")
self.act_options.setObjectName("act_options")
self.act_program = QtWidgets.QAction(win_revpicommander)
self.act_program.setShortcut("Ctrl+P")
self.act_program.setObjectName("act_program")
self.act_developer = QtWidgets.QAction(win_revpicommander)
self.act_developer.setShortcut("Ctrl+D")
self.act_developer.setObjectName("act_developer")
self.act_pictory = QtWidgets.QAction(win_revpicommander)
self.act_pictory.setObjectName("act_pictory")
self.act_disconnect = QtWidgets.QAction(win_revpicommander)
self.act_disconnect.setShortcut("Ctrl+X")
self.act_disconnect.setObjectName("act_disconnect")
self.act_reset = QtWidgets.QAction(win_revpicommander)
self.act_reset.setObjectName("act_reset")
@@ -117,7 +157,7 @@ class Ui_win_revpicommander(object):
self.menubar.addAction(self.men_help.menuAction())
self.retranslateUi(win_revpicommander)
self.act_quit.triggered.connect(win_revpicommander.close)
self.act_quit.triggered.connect(win_revpicommander.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(win_revpicommander)
win_revpicommander.setTabOrder(self.btn_plc_start, self.btn_plc_stop)
win_revpicommander.setTabOrder(self.btn_plc_stop, self.btn_plc_restart)
@@ -126,11 +166,11 @@ class Ui_win_revpicommander(object):
def retranslateUi(self, win_revpicommander):
_translate = QtCore.QCoreApplication.translate
win_revpicommander.setWindowTitle(_translate("win_revpicommander", "RevPi Python PLC Commander"))
self.btn_plc_start.setText(_translate("win_revpicommander", "PLC &start"))
self.btn_plc_stop.setText(_translate("win_revpicommander", "PLC s&top"))
self.btn_plc_restart.setText(_translate("win_revpicommander", "PLC restart"))
self.btn_plc_logs.setText(_translate("win_revpicommander", "PLC &logs"))
self.lbl_status.setText(_translate("win_revpicommander", "Status:"))
self.btn_plc_debug.setText(_translate("win_revpicommander", "PLC watch &mode"))
self.men_file.setTitle(_translate("win_revpicommander", "&File"))
self.men_help.setTitle(_translate("win_revpicommander", "&Help"))
@@ -138,23 +178,17 @@ class Ui_win_revpicommander(object):
self.men_connections.setTitle(_translate("win_revpicommander", "&Connections"))
self.act_connections.setText(_translate("win_revpicommander", "&Connections..."))
self.act_search.setText(_translate("win_revpicommander", "&Search Revolution Pi..."))
self.act_search.setShortcut(_translate("win_revpicommander", "Ctrl+F"))
self.act_quit.setText(_translate("win_revpicommander", "&Quit"))
self.act_webpage.setText(_translate("win_revpicommander", "Visit &webpage..."))
self.act_info.setText(_translate("win_revpicommander", "&Info..."))
self.act_logs.setText(_translate("win_revpicommander", "PLC &logs..."))
self.act_logs.setShortcut(_translate("win_revpicommander", "F3"))
self.act_options.setText(_translate("win_revpicommander", "PLC &options..."))
self.act_options.setShortcut(_translate("win_revpicommander", "Ctrl+O"))
self.act_program.setText(_translate("win_revpicommander", "PLC progra&m..."))
self.act_program.setShortcut(_translate("win_revpicommander", "Ctrl+P"))
self.act_developer.setText(_translate("win_revpicommander", "PLC de&veloper..."))
self.act_developer.setShortcut(_translate("win_revpicommander", "F9"))
self.act_pictory.setText(_translate("win_revpicommander", "piCtory configuraiton..."))
self.act_disconnect.setText(_translate("win_revpicommander", "&Disconnect"))
self.act_disconnect.setShortcut(_translate("win_revpicommander", "Ctrl+X"))
self.act_reset.setText(_translate("win_revpicommander", "Reset driver..."))
self.act_simulator.setText(_translate("win_revpicommander", "Start local si&mulator..."))
self.act_simulator.setText(_translate("win_revpicommander", "RevPi si&mulator..."))
from . import ressources_rc

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'revpiinfo.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -41,6 +41,7 @@ class Ui_diag_revpiinfo(object):
font.setBold(True)
font.setWeight(75)
self.lbl_version_pyload.setFont(font)
self.lbl_version_pyload.setText("0.0.0")
self.lbl_version_pyload.setAlignment(QtCore.Qt.AlignCenter)
self.lbl_version_pyload.setObjectName("lbl_version_pyload")
self.gridLayout.addWidget(self.lbl_version_pyload, 3, 1, 1, 1)
@@ -66,6 +67,7 @@ class Ui_diag_revpiinfo(object):
font = QtGui.QFont()
font.setPointSize(14)
self.lbl_version_control.setFont(font)
self.lbl_version_control.setText("0.0.0")
self.lbl_version_control.setObjectName("lbl_version_control")
self.horizontalLayout.addWidget(self.lbl_version_control)
self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 3)
@@ -78,8 +80,8 @@ class Ui_diag_revpiinfo(object):
self.gridLayout.addItem(spacerItem, 5, 0, 1, 1)
self.retranslateUi(diag_revpiinfo)
self.btn_box.accepted.connect(diag_revpiinfo.accept)
self.btn_box.rejected.connect(diag_revpiinfo.reject)
self.btn_box.accepted.connect(diag_revpiinfo.accept) # type: ignore
self.btn_box.rejected.connect(diag_revpiinfo.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_revpiinfo)
def retranslateUi(self, diag_revpiinfo):
@@ -87,10 +89,8 @@ class Ui_diag_revpiinfo(object):
diag_revpiinfo.setWindowTitle(_translate("diag_revpiinfo", "Program information"))
self.lbl_head.setText(_translate("diag_revpiinfo", "RevPi Python PLC - Commander"))
self.lbl_lbl_version_pyload.setText(_translate("diag_revpiinfo", "RevPiPyLoad version on RevPi:"))
self.lbl_version_pyload.setText(_translate("diag_revpiinfo", "0.0.0"))
self.lbl_link.setText(_translate("diag_revpiinfo", "<html><head/><body><p><a href=\"https://revpimodio.org/\"><span style=\" text-decoration: underline; color:#0000ff;\">https://revpimodio.org/</span></a></p></body></html>"))
self.lbl_lbl_version_control.setText(_translate("diag_revpiinfo", "Version:"))
self.lbl_version_control.setText(_translate("diag_revpiinfo", "0.0.0"))
self.lbl_info.setText(_translate("diag_revpiinfo", "RevPiModIO, RevPiPyLoad and RevPiPyControl are community driven projects. They are all free and open source software.\n"
"All of them comes with ABSOLUTELY NO WARRANTY, to the extent permitted by\n"
"applicable law.\n"

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'revpilogfile.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'revpioption.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -116,8 +116,8 @@ class Ui_diag_options(object):
self.verticalLayout.addWidget(self.btn_box)
self.retranslateUi(diag_options)
self.btn_box.accepted.connect(diag_options.accept)
self.btn_box.rejected.connect(diag_options.reject)
self.btn_box.accepted.connect(diag_options.accept) # type: ignore
self.btn_box.rejected.connect(diag_options.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_options)
def retranslateUi(self, diag_options):

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'revpiplclist.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,9 +14,98 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_diag_connections(object):
def setupUi(self, diag_connections):
diag_connections.setObjectName("diag_connections")
diag_connections.resize(520, 508)
diag_connections.resize(496, 569)
self.gridLayout = QtWidgets.QGridLayout(diag_connections)
self.gridLayout.setObjectName("gridLayout")
self.tab_properties = QtWidgets.QTabWidget(diag_connections)
self.tab_properties.setObjectName("tab_properties")
self.tab_connection = QtWidgets.QWidget()
self.tab_connection.setObjectName("tab_connection")
self.formLayout_2 = QtWidgets.QFormLayout(self.tab_connection)
self.formLayout_2.setObjectName("formLayout_2")
self.lbl_name = QtWidgets.QLabel(self.tab_connection)
self.lbl_name.setObjectName("lbl_name")
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_name)
self.txt_name = QtWidgets.QLineEdit(self.tab_connection)
self.txt_name.setObjectName("txt_name")
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.txt_name)
self.lbl_address = QtWidgets.QLabel(self.tab_connection)
self.lbl_address.setObjectName("lbl_address")
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_address)
self.txt_address = QtWidgets.QLineEdit(self.tab_connection)
self.txt_address.setObjectName("txt_address")
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.txt_address)
self.lbl_port = QtWidgets.QLabel(self.tab_connection)
self.lbl_port.setObjectName("lbl_port")
self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.lbl_port)
self.sbx_port = QtWidgets.QSpinBox(self.tab_connection)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sbx_port.sizePolicy().hasHeightForWidth())
self.sbx_port.setSizePolicy(sizePolicy)
self.sbx_port.setMinimum(1)
self.sbx_port.setMaximum(65535)
self.sbx_port.setProperty("value", 55123)
self.sbx_port.setObjectName("sbx_port")
self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.sbx_port)
self.lbl_timeout = QtWidgets.QLabel(self.tab_connection)
self.lbl_timeout.setObjectName("lbl_timeout")
self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_timeout)
self.sbx_timeout = QtWidgets.QSpinBox(self.tab_connection)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sbx_timeout.sizePolicy().hasHeightForWidth())
self.sbx_timeout.setSizePolicy(sizePolicy)
self.sbx_timeout.setMinimum(5)
self.sbx_timeout.setMaximum(30)
self.sbx_timeout.setObjectName("sbx_timeout")
self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.sbx_timeout)
self.lbl_folder = QtWidgets.QLabel(self.tab_connection)
self.lbl_folder.setObjectName("lbl_folder")
self.formLayout_2.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.lbl_folder)
self.cbb_folder = QtWidgets.QComboBox(self.tab_connection)
self.cbb_folder.setEditable(True)
self.cbb_folder.setObjectName("cbb_folder")
self.cbb_folder.addItem("")
self.cbb_folder.setItemText(0, "")
self.formLayout_2.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.cbb_folder)
self.tab_properties.addTab(self.tab_connection, "")
self.tab_ssh = QtWidgets.QWidget()
self.tab_ssh.setObjectName("tab_ssh")
self.formLayout = QtWidgets.QFormLayout(self.tab_ssh)
self.formLayout.setObjectName("formLayout")
self.lbl_ssh_use_tunnel = QtWidgets.QLabel(self.tab_ssh)
self.lbl_ssh_use_tunnel.setObjectName("lbl_ssh_use_tunnel")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_ssh_use_tunnel)
self.cbx_ssh_use_tunnel = QtWidgets.QCheckBox(self.tab_ssh)
self.cbx_ssh_use_tunnel.setText("")
self.cbx_ssh_use_tunnel.setChecked(True)
self.cbx_ssh_use_tunnel.setObjectName("cbx_ssh_use_tunnel")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.cbx_ssh_use_tunnel)
self.lbl_ssh_port = QtWidgets.QLabel(self.tab_ssh)
self.lbl_ssh_port.setObjectName("lbl_ssh_port")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_ssh_port)
self.sbx_ssh_port = QtWidgets.QSpinBox(self.tab_ssh)
self.sbx_ssh_port.setMaximum(65535)
self.sbx_ssh_port.setProperty("value", 22)
self.sbx_ssh_port.setObjectName("sbx_ssh_port")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.sbx_ssh_port)
self.lbl_ssh_user = QtWidgets.QLabel(self.tab_ssh)
self.lbl_ssh_user.setObjectName("lbl_ssh_user")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.lbl_ssh_user)
self.txt_ssh_user = QtWidgets.QLineEdit(self.tab_ssh)
self.txt_ssh_user.setText("pi")
self.txt_ssh_user.setObjectName("txt_ssh_user")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.txt_ssh_user)
self.tab_properties.addTab(self.tab_ssh, "")
self.gridLayout.addWidget(self.tab_properties, 1, 0, 1, 2)
self.btn_box = QtWidgets.QDialogButtonBox(diag_connections)
self.btn_box.setOrientation(QtCore.Qt.Horizontal)
self.btn_box.setStandardButtons(QtWidgets.QDialogButtonBox.Discard|QtWidgets.QDialogButtonBox.Save)
self.btn_box.setObjectName("btn_box")
self.gridLayout.addWidget(self.btn_box, 2, 0, 1, 2)
self.tre_connections = QtWidgets.QTreeWidget(diag_connections)
self.tre_connections.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.tre_connections.setObjectName("tre_connections")
@@ -54,67 +143,29 @@ class Ui_diag_connections(object):
self.btn_add.setObjectName("btn_add")
self.vl_edit.addWidget(self.btn_add)
self.gridLayout.addLayout(self.vl_edit, 0, 1, 1, 1)
self.gb_properties = QtWidgets.QGroupBox(diag_connections)
self.gb_properties.setObjectName("gb_properties")
self.formLayout = QtWidgets.QFormLayout(self.gb_properties)
self.formLayout.setObjectName("formLayout")
self.lbl_name = QtWidgets.QLabel(self.gb_properties)
self.lbl_name.setObjectName("lbl_name")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_name)
self.lbl_folder = QtWidgets.QLabel(self.gb_properties)
self.lbl_folder.setObjectName("lbl_folder")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.lbl_folder)
self.lbl_address = QtWidgets.QLabel(self.gb_properties)
self.lbl_address.setObjectName("lbl_address")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_address)
self.lbl_port = QtWidgets.QLabel(self.gb_properties)
self.lbl_port.setObjectName("lbl_port")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.lbl_port)
self.txt_name = QtWidgets.QLineEdit(self.gb_properties)
self.txt_name.setObjectName("txt_name")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.txt_name)
self.txt_address = QtWidgets.QLineEdit(self.gb_properties)
self.txt_address.setObjectName("txt_address")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.txt_address)
self.sbx_port = QtWidgets.QSpinBox(self.gb_properties)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sbx_port.sizePolicy().hasHeightForWidth())
self.sbx_port.setSizePolicy(sizePolicy)
self.sbx_port.setMinimum(1)
self.sbx_port.setMaximum(65535)
self.sbx_port.setProperty("value", 55123)
self.sbx_port.setObjectName("sbx_port")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.sbx_port)
self.cbb_folder = QtWidgets.QComboBox(self.gb_properties)
self.cbb_folder.setEditable(True)
self.cbb_folder.setObjectName("cbb_folder")
self.cbb_folder.addItem("")
self.cbb_folder.setItemText(0, "")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.cbb_folder)
self.gridLayout.addWidget(self.gb_properties, 1, 0, 1, 2)
self.btn_box = QtWidgets.QDialogButtonBox(diag_connections)
self.btn_box.setOrientation(QtCore.Qt.Horizontal)
self.btn_box.setStandardButtons(QtWidgets.QDialogButtonBox.Discard|QtWidgets.QDialogButtonBox.Save)
self.btn_box.setObjectName("btn_box")
self.gridLayout.addWidget(self.btn_box, 2, 0, 1, 2)
self.retranslateUi(diag_connections)
self.btn_box.accepted.connect(diag_connections.accept)
self.btn_box.rejected.connect(diag_connections.reject)
self.tab_properties.setCurrentIndex(0)
self.btn_box.accepted.connect(diag_connections.accept) # type: ignore
self.btn_box.rejected.connect(diag_connections.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_connections)
def retranslateUi(self, diag_connections):
_translate = QtCore.QCoreApplication.translate
diag_connections.setWindowTitle(_translate("diag_connections", "Revolution Pi connections"))
self.tre_connections.headerItem().setText(0, _translate("diag_connections", "Connection name"))
self.tre_connections.headerItem().setText(1, _translate("diag_connections", "Address"))
self.gb_properties.setTitle(_translate("diag_connections", "Connection properties"))
self.lbl_name.setText(_translate("diag_connections", "Display name:"))
self.lbl_folder.setText(_translate("diag_connections", "Sub folder:"))
self.lbl_address.setText(_translate("diag_connections", "Address (DNS/IP):"))
self.lbl_port.setText(_translate("diag_connections", "Port (Default {0}):"))
self.lbl_timeout.setText(_translate("diag_connections", "Connection timeout:"))
self.sbx_timeout.setSuffix(_translate("diag_connections", " sec."))
self.lbl_folder.setText(_translate("diag_connections", "Sub folder:"))
self.tab_properties.setTabText(self.tab_properties.indexOf(self.tab_connection), _translate("diag_connections", "Connection"))
self.lbl_ssh_use_tunnel.setText(_translate("diag_connections", "Connect over SSH tunnel:"))
self.lbl_ssh_port.setText(_translate("diag_connections", "SSH port:"))
self.lbl_ssh_user.setText(_translate("diag_connections", "SSH user name:"))
self.tab_properties.setTabText(self.tab_properties.indexOf(self.tab_ssh), _translate("diag_connections", "Over SSH"))
self.tre_connections.headerItem().setText(0, _translate("diag_connections", "Connection name"))
self.tre_connections.headerItem().setText(1, _translate("diag_connections", "Address"))
from . import ressources_rc

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'revpiprogram.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -105,8 +105,8 @@ class Ui_diag_program(object):
self.verticalLayout.addWidget(self.btn_box)
self.retranslateUi(diag_program)
self.btn_box.accepted.connect(diag_program.accept)
self.btn_box.rejected.connect(diag_program.reject)
self.btn_box.accepted.connect(diag_program.accept) # type: ignore
self.btn_box.rejected.connect(diag_program.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_program)
diag_program.setTabOrder(self.cbb_plcprogram, self.txt_plcarguments)
diag_program.setTabOrder(self.txt_plcarguments, self.rbn_pythonversion_2)

View File

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'simulator.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
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.setShortcut("Ctrl+1")
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.setShortcut("Ctrl+2")
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.setShortcut("Ctrl+3")
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) # type: ignore
self.btn_start_nochange.clicked.connect(diag_simulator.accept) # type: ignore
self.btn_start_pictory.clicked.connect(diag_simulator.accept) # type: ignore
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", "select..."))
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 file"))
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 you call RevPiModIO with this additional parameters:"))
self.btn_start_pictory.setText(_translate("diag_simulator", "Start with piCtory default values"))
self.btn_start_empty.setText(_translate("diag_simulator", "Start with empty process image"))
self.btn_start_nochange.setText(_translate("diag_simulator", "Start without changing actual process image"))
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_())

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'sshauth.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_diag_sshauth(object):
def setupUi(self, diag_sshauth):
diag_sshauth.setObjectName("diag_sshauth")
diag_sshauth.setWindowModality(QtCore.Qt.ApplicationModal)
diag_sshauth.resize(363, 163)
self.verticalLayout = QtWidgets.QVBoxLayout(diag_sshauth)
self.verticalLayout.setObjectName("verticalLayout")
self.wid_password = QtWidgets.QWidget(diag_sshauth)
self.wid_password.setObjectName("wid_password")
self.formLayout = QtWidgets.QFormLayout(self.wid_password)
self.formLayout.setObjectName("formLayout")
self.lbl_username = QtWidgets.QLabel(self.wid_password)
self.lbl_username.setObjectName("lbl_username")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lbl_username)
self.lbl_password = QtWidgets.QLabel(self.wid_password)
self.lbl_password.setObjectName("lbl_password")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lbl_password)
self.txt_password = QtWidgets.QLineEdit(self.wid_password)
self.txt_password.setEchoMode(QtWidgets.QLineEdit.Password)
self.txt_password.setObjectName("txt_password")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.txt_password)
self.txt_username = QtWidgets.QLineEdit(self.wid_password)
self.txt_username.setObjectName("txt_username")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.txt_username)
self.verticalLayout.addWidget(self.wid_password)
self.cbx_save_password = QtWidgets.QCheckBox(diag_sshauth)
self.cbx_save_password.setObjectName("cbx_save_password")
self.verticalLayout.addWidget(self.cbx_save_password)
self.btn_box = QtWidgets.QDialogButtonBox(diag_sshauth)
self.btn_box.setOrientation(QtCore.Qt.Horizontal)
self.btn_box.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.btn_box.setObjectName("btn_box")
self.verticalLayout.addWidget(self.btn_box)
self.retranslateUi(diag_sshauth)
self.btn_box.accepted.connect(diag_sshauth.accept) # type: ignore
self.btn_box.rejected.connect(diag_sshauth.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(diag_sshauth)
def retranslateUi(self, diag_sshauth):
_translate = QtCore.QCoreApplication.translate
diag_sshauth.setWindowTitle(_translate("diag_sshauth", "SSH authentication"))
self.lbl_username.setText(_translate("diag_sshauth", "SSH username:"))
self.lbl_password.setText(_translate("diag_sshauth", "SSH password:"))
self.cbx_save_password.setToolTip(_translate("diag_sshauth", "Username and password will be saved in secured operating systems\'s password storage."))
self.cbx_save_password.setText(_translate("diag_sshauth", "Save username and password"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
diag_sshauth = QtWidgets.QDialog()
ui = Ui_diag_sshauth()
ui.setupUi(diag_sshauth)
diag_sshauth.show()
sys.exit(app.exec_())

View File

@@ -1,6 +0,0 @@
[DEFAULT]
Debian-Version=1
Depends3=python3-pyqt5, python3-zeroconf
Section=universe/x11
Suite=stable
X-Python3-Version: >=3.4

View File

@@ -1,29 +1,35 @@
SOURCES = revpicommander/aclmanager.py \
revpicommander/avahisearch.py \
revpicommander/debugcontrol.py \
revpicommander/debugios.py \
revpicommander/mqttmanager.py \
revpicommander/revpifiles.py \
revpicommander/revpiinfo.py \
revpicommander/revpilogfile.py \
revpicommander/revpioption.py \
revpicommander/revpiplclist.py \
revpicommander/revpiprogram.py \
revpicommander/revpicommander.py
SOURCES = src/revpicommander/aclmanager.py \
src/revpicommander/avahisearch.py \
src/revpicommander/debugcontrol.py \
src/revpicommander/debugios.py \
src/revpicommander/helper.py \
src/revpicommander/mqttmanager.py \
src/revpicommander/revpifiles.py \
src/revpicommander/revpiinfo.py \
src/revpicommander/revpilogfile.py \
src/revpicommander/revpioption.py \
src/revpicommander/revpiplclist.py \
src/revpicommander/revpiprogram.py \
src/revpicommander/simulator.py \
src/revpicommander/sshauth.py \
src/revpicommander/revpicommander.py
FORMS = include/ui_dev/aclmanager.ui \
include/ui_dev/avahisearch.ui \
include/ui_dev/debugcontrol.ui \
include/ui_dev/debugios.ui \
include/ui_dev/mqttmanager.ui \
include/ui_dev/files.ui \
include/ui_dev/revpiinfo.ui \
include/ui_dev/revpilogfile.ui \
include/ui_dev/revpioption.ui \
include/ui_dev/revpiplclist.ui \
include/ui_dev/revpiprogram.ui \
include/ui_dev/revpicommander.ui
FORMS = ui_dev/aclmanager.ui \
ui_dev/avahisearch.ui \
ui_dev/backgroundworker.ui \
ui_dev/debugcontrol.ui \
ui_dev/debugios.ui \
ui_dev/files.ui \
ui_dev/mqttmanager.ui \
ui_dev/revpiinfo.ui \
ui_dev/revpilogfile.ui \
ui_dev/revpioption.ui \
ui_dev/revpiplclist.ui \
ui_dev/revpiprogram.ui \
ui_dev/simulator.ui \
ui_dev/sshauth.ui \
ui_dev/revpicommander.ui
TRANSLATIONS = revpicommander/locale/revpicommander_de.ts
TRANSLATIONS = src/revpicommander/locale/revpicommander_de.ts
CODECRORTR = UTF-8

View File

@@ -145,7 +145,7 @@
<item>
<widget class="QLabel" name="lbl_ip_a">
<property name="text">
<string>.</string>
<string notr="true">.</string>
</property>
</widget>
</item>
@@ -159,7 +159,7 @@
<item>
<widget class="QLabel" name="lbl_ip_b">
<property name="text">
<string>.</string>
<string notr="true">.</string>
</property>
</widget>
</item>
@@ -173,7 +173,7 @@
<item>
<widget class="QLabel" name="lbl_ip_c">
<property name="text">
<string>.</string>
<string notr="true">.</string>
</property>
</widget>
</item>

View File

@@ -120,6 +120,45 @@
</widget>
</item>
</layout>
<action name="act_copy_host">
<property name="text">
<string>Copy host name</string>
</property>
</action>
<action name="act_copy_ip">
<property name="text">
<string>Copy IP address</string>
</property>
</action>
<action name="act_open_pictory">
<property name="text">
<string>Open piCtory</string>
</property>
</action>
<action name="act_connect_ssh">
<property name="text">
<string>Connect via SSH (recommended)</string>
</property>
<property name="toolTip">
<string>Establish a connection via encrypted SSH tunnel</string>
</property>
</action>
<action name="act_connect_xmlrpc">
<property name="text">
<string>Connect via XML-RPC</string>
</property>
<property name="toolTip">
<string>You have to configure your Revolution Pi to accept this connections</string>
</property>
</action>
<action name="act_connect">
<property name="text">
<string>Connect</string>
</property>
<property name="toolTip">
<string>Connect to Revoluton Pi</string>
</property>
</action>
</widget>
<resources>
<include location="ressources.qrc"/>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>diag_backgroundworker</class>
<widget class="QDialog" name="diag_backgroundworker">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>418</width>
<height>97</height>
</rect>
</property>
<property name="windowTitle">
<string>File transfer...</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="lbl_status">
<property name="text">
<string notr="true">Status message...</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="pgb_status">
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="btn_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -53,26 +53,42 @@
<item>
<widget class="QPushButton" name="btn_read_io">
<property name="toolTip">
<string>Read all IO values and discard local changes (F4)</string>
<string>Read all IO values and discard local changes (F4)
Hold this button pressed and it will refresh the IOs every 200 ms.</string>
</property>
<property name="text">
<string>Read &amp;all IO values</string>
</property>
<property name="shortcut">
<string>F4</string>
<property name="autoRepeat">
<bool>true</bool>
</property>
<property name="autoRepeatDelay">
<number>100</number>
</property>
<property name="autoRepeatInterval">
<number>200</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_refresh_io">
<property name="toolTip">
<string>Refresh all IO values which are locally not changed (F5)</string>
<string>Refresh all IO values which are locally not changed (F5)
Hold this button pressed and it will refresh the IOs every 200 ms.</string>
</property>
<property name="text">
<string>&amp;Refresh unchanged IOs</string>
</property>
<property name="shortcut">
<string>F5</string>
<property name="autoRepeat">
<bool>true</bool>
</property>
<property name="autoRepeatDelay">
<number>100</number>
</property>
<property name="autoRepeatInterval">
<number>200</number>
</property>
</widget>
</item>
@@ -84,9 +100,6 @@
<property name="text">
<string>&amp;Write changed outputs</string>
</property>
<property name="shortcut">
<string>F6</string>
</property>
</widget>
</item>
<item>

View File

@@ -84,10 +84,10 @@
</sizepolicy>
</property>
<property name="toolTip">
<string>/</string>
<string notr="true">/</string>
</property>
<property name="text">
<string>/</string>
<string notr="true">/</string>
</property>
</widget>
</item>
@@ -180,10 +180,10 @@
</sizepolicy>
</property>
<property name="toolTip">
<string>/</string>
<string notr="true">/</string>
</property>
<property name="text">
<string>/</string>
<string notr="true">/</string>
</property>
</widget>
</item>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
ui_dev/ico/edit-find.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
ui_dev/ico/process-stop.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
ui_dev/ico/system-run.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
ui_dev/ico/system-search.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
ui_dev/ico/view-refresh.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -9,6 +9,12 @@
<file>ico/file-python.ico</file>
</qresource>
<qresource prefix="action">
<file>ico/applications-utilities.ico</file>
<file>ico/edit-find.ico</file>
<file>ico/process-stop.ico</file>
<file>ico/system-run.ico</file>
<file>ico/system-search.ico</file>
<file>ico/view-refresh.ico</file>
<file>ico/arrow-left.ico</file>
<file>ico/arrow-right.ico</file>
<file>ico/folder-open.ico</file>

View File

@@ -7,71 +7,121 @@
<rect>
<x>0</x>
<y>0</y>
<width>318</width>
<height>273</height>
<width>353</width>
<height>299</height>
</rect>
</property>
<property name="windowTitle">
<string>RevPi Python PLC Commander</string>
<string notr="true">RevPi Commander</string>
</property>
<property name="windowIcon">
<iconset resource="ressources.qrc">
<normaloff>:/main/ico/revpipycontrol.ico</normaloff>:/main/ico/revpipycontrol.ico</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gl">
<item row="0" column="0">
<widget class="QLineEdit" name="txt_connection">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
<layout class="QVBoxLayout" name="gl">
<item>
<layout class="QHBoxLayout" name="hzl_connection">
<item>
<widget class="QLineEdit" name="txt_host">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="txt_connection">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<item>
<widget class="QPushButton" name="btn_plc_start">
<property name="text">
<string>PLC &amp;start</string>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/system-run.ico</normaloff>:/action/ico/system-run.ico</iconset>
</property>
</widget>
</item>
<item row="2" column="0">
<item>
<widget class="QPushButton" name="btn_plc_stop">
<property name="text">
<string>PLC s&amp;top</string>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/process-stop.ico</normaloff>:/action/ico/process-stop.ico</iconset>
</property>
</widget>
</item>
<item row="3" column="0">
<item>
<widget class="QPushButton" name="btn_plc_restart">
<property name="text">
<string>PLC restart</string>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/view-refresh.ico</normaloff>:/action/ico/view-refresh.ico</iconset>
</property>
</widget>
</item>
<item row="4" column="0">
<item>
<widget class="QPushButton" name="btn_plc_logs">
<property name="text">
<string>PLC &amp;logs</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLineEdit" name="txt_status">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/applications-utilities.ico</normaloff>:/action/ico/applications-utilities.ico</iconset>
</property>
</widget>
</item>
<item row="6" column="0">
<item>
<layout class="QHBoxLayout" name="hzl_status">
<item>
<widget class="QLabel" name="lbl_status">
<property name="text">
<string>Status:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="txt_status">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="btn_plc_debug">
<property name="minimumSize">
<size>
@@ -82,6 +132,10 @@
<property name="text">
<string>PLC watch &amp;mode</string>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/edit-find.ico</normaloff>:/action/ico/edit-find.ico</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
@@ -94,8 +148,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>318</width>
<height>22</height>
<width>353</width>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="men_file">
@@ -150,13 +204,16 @@
<property name="text">
<string>&amp;Connections...</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+N</string>
</property>
</action>
<action name="act_search">
<property name="text">
<string>&amp;Search Revolution Pi...</string>
</property>
<property name="shortcut">
<string>Ctrl+F</string>
<string notr="true">Ctrl+F</string>
</property>
</action>
<action name="act_quit">
@@ -179,7 +236,7 @@
<string>PLC &amp;logs...</string>
</property>
<property name="shortcut">
<string>F3</string>
<string notr="true">Ctrl+L</string>
</property>
</action>
<action name="act_options">
@@ -187,7 +244,7 @@
<string>PLC &amp;options...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
<string notr="true">Ctrl+O</string>
</property>
</action>
<action name="act_program">
@@ -195,7 +252,7 @@
<string>PLC progra&amp;m...</string>
</property>
<property name="shortcut">
<string>Ctrl+P</string>
<string notr="true">Ctrl+P</string>
</property>
</action>
<action name="act_developer">
@@ -203,7 +260,7 @@
<string>PLC de&amp;veloper...</string>
</property>
<property name="shortcut">
<string>F9</string>
<string notr="true">Ctrl+D</string>
</property>
</action>
<action name="act_pictory">
@@ -216,7 +273,7 @@
<string>&amp;Disconnect</string>
</property>
<property name="shortcut">
<string>Ctrl+X</string>
<string notr="true">Ctrl+X</string>
</property>
</action>
<action name="act_reset">
@@ -226,7 +283,7 @@
</action>
<action name="act_simulator">
<property name="text">
<string>Start local si&amp;mulator...</string>
<string>RevPi si&amp;mulator...</string>
</property>
</action>
</widget>

View File

@@ -56,7 +56,7 @@
</font>
</property>
<property name="text">
<string>0.0.0</string>
<string notr="true">0.0.0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
@@ -108,7 +108,7 @@
</font>
</property>
<property name="text">
<string>0.0.0</string>
<string notr="true">0.0.0</string>
</property>
</widget>
</item>

304
ui_dev/revpiplclist.ui Normal file
View File

@@ -0,0 +1,304 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>diag_connections</class>
<widget class="QDialog" name="diag_connections">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>496</width>
<height>569</height>
</rect>
</property>
<property name="windowTitle">
<string>Revolution Pi connections</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="2">
<widget class="QTabWidget" name="tab_properties">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_connection">
<attribute name="title">
<string>Connection</string>
</attribute>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="lbl_name">
<property name="text">
<string>Display name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txt_name"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_address">
<property name="text">
<string>Address (DNS/IP):</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="txt_address"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lbl_port">
<property name="text">
<string>Port (Default {0}):</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="sbx_port">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>55123</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lbl_timeout">
<property name="text">
<string>Connection timeout:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="sbx_timeout">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string> sec.</string>
</property>
<property name="minimum">
<number>5</number>
</property>
<property name="maximum">
<number>30</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lbl_folder">
<property name="text">
<string>Sub folder:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="cbb_folder">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string/>
</property>
</item>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_ssh">
<attribute name="title">
<string>Over SSH</string>
</attribute>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="lbl_ssh_use_tunnel">
<property name="text">
<string>Connect over SSH tunnel:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="cbx_ssh_use_tunnel">
<property name="text">
<string notr="true"/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_ssh_port">
<property name="text">
<string>SSH port:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="sbx_ssh_port">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>22</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lbl_ssh_user">
<property name="text">
<string>SSH user name:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="txt_ssh_user">
<property name="text">
<string notr="true">pi</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="btn_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Discard|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QTreeWidget" name="tre_connections">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<column>
<property name="text">
<string>Connection name</string>
</property>
</column>
<column>
<property name="text">
<string>Address</string>
</property>
</column>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="vl_edit">
<item>
<spacer name="vs_edit">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btn_up">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/arrow-up.ico</normaloff>:/action/ico/arrow-up.ico</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_down">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/arrow-down.ico</normaloff>:/action/ico/arrow-down.ico</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_delete">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/edit-delete.ico</normaloff>:/action/ico/edit-delete.ico</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_add">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ressources.qrc">
<normaloff>:/action/ico/edit-add.ico</normaloff>:/action/ico/edit-add.ico</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="ressources.qrc"/>
</resources>
<connections>
<connection>
<sender>btn_box</sender>
<signal>accepted()</signal>
<receiver>diag_connections</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>btn_box</sender>
<signal>rejected()</signal>
<receiver>diag_connections</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

245
ui_dev/simulator.ui Normal file
View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>diag_simulator</class>
<widget class="QDialog" name="diag_simulator">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>500</height>
</rect>
</property>
<property name="windowTitle">
<string>piControl simulator</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="gb_settings">
<property name="title">
<string>Simulator settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="lbl_history">
<property name="text">
<string>Last used:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_configrsc">
<property name="text">
<string>piCtory file:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="txt_configrsc">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btn_configrsc">
<property name="text">
<string>select...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lbl_procimg">
<property name="text">
<string>Process image:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="txt_procimg">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lbl_stop">
<property name="text">
<string>Stop action:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lbl_restart">
<property name="text">
<string>Restart action:</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QComboBox" name="cbb_history">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLength</enum>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QCheckBox" name="cbx_stop_remove">
<property name="text">
<string>Remove process image file</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QRadioButton" name="rb_restart_pictory">
<property name="text">
<string>Restore piCtory default values</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="QRadioButton" name="rb_restart_zero">
<property name="text">
<string>Reset everything to ZERO</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gb_info">
<property name="title">
<string>RevPiModIO integration</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="lbl_info">
<property name="text">
<string>You can work with this simulator if you call RevPiModIO with this additional parameters:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="txt_info">
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string notr="true"/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_start_pictory">
<property name="text">
<string>Start with piCtory default values</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+1</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_start_empty">
<property name="text">
<string>Start with empty process image</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+2</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_start_nochange">
<property name="text">
<string>Start without changing actual process image</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+3</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>cbb_history</tabstop>
<tabstop>btn_configrsc</tabstop>
<tabstop>cbx_stop_remove</tabstop>
<tabstop>rb_restart_pictory</tabstop>
<tabstop>rb_restart_zero</tabstop>
<tabstop>txt_info</tabstop>
<tabstop>btn_start_pictory</tabstop>
<tabstop>btn_start_empty</tabstop>
<tabstop>btn_start_nochange</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>btn_start_empty</sender>
<signal>clicked()</signal>
<receiver>diag_simulator</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>268</x>
<y>447</y>
</hint>
<hint type="destinationlabel">
<x>268</x>
<y>249</y>
</hint>
</hints>
</connection>
<connection>
<sender>btn_start_nochange</sender>
<signal>clicked()</signal>
<receiver>diag_simulator</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>268</x>
<y>478</y>
</hint>
<hint type="destinationlabel">
<x>268</x>
<y>249</y>
</hint>
</hints>
</connection>
<connection>
<sender>btn_start_pictory</sender>
<signal>clicked()</signal>
<receiver>diag_simulator</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>268</x>
<y>416</y>
</hint>
<hint type="destinationlabel">
<x>268</x>
<y>249</y>
</hint>
</hints>
</connection>
</connections>
</ui>

107
ui_dev/sshauth.ui Normal file
View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>diag_sshauth</class>
<widget class="QDialog" name="diag_sshauth">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>363</width>
<height>163</height>
</rect>
</property>
<property name="windowTitle">
<string>SSH authentication</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="wid_password" native="true">
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="lbl_username">
<property name="text">
<string>SSH username:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_password">
<property name="text">
<string>SSH password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="txt_password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txt_username"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbx_save_password">
<property name="toolTip">
<string>Username and password will be saved in secured operating systems's password storage.</string>
</property>
<property name="text">
<string>Save username and password</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="btn_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>btn_box</sender>
<signal>accepted()</signal>
<receiver>diag_sshauth</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>btn_box</sender>
<signal>rejected()</signal>
<receiver>diag_sshauth</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>