From ab7ab78d51ade137ad2b47e7edc0b26099f45801 Mon Sep 17 00:00:00 2001 From: Jan Rach <rachj@students.zcu.cz> Date: Mon, 24 May 2021 08:31:08 +0000 Subject: [PATCH 01/12] Feature/8923 grid layout --- aswi2021vochomurka/view/main_view.py | 96 ++++++++++++++++------------ 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index a769968..0f154a2 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -1,29 +1,17 @@ import logging -import math -import random +import matplotlib.pyplot as plt +from PyQt5 import QtCore from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal -from PyQt5.QtWidgets import QMainWindow, QPlainTextEdit, QDialog, QHBoxLayout -from numpy import pi, sin, cos, tan, exp -from matplotlib.pyplot import subplot +from PyQt5.QtWidgets import QDialog, QPushButton, QVBoxLayout +from PyQt5.QtWidgets import QHBoxLayout, QGridLayout, QScrollArea, QWidget +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from aswi2021vochomurka.model.Message import Message from aswi2021vochomurka.service.mqtt.mqtt_subscriber import MQTTSubscriber from aswi2021vochomurka.service.subscriber import Subscriber from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams - -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar -import matplotlib.pyplot as plt - -import sys -from PyQt5.QtWidgets import QDialog, QApplication, QPushButton, QVBoxLayout -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar -import matplotlib.pyplot as plt -import random - from aswi2021vochomurka.view.logger_view import LoggerView @@ -31,11 +19,11 @@ class Worker(QObject, SubscriberCallback): connected = pyqtSignal() disconnected = pyqtSignal() error = pyqtSignal(Exception) - newMessage = pyqtSignal(str) + newMessage = pyqtSignal(Message) subscriber: Subscriber = None params = SubscriberParams( - ["/home/1", "/home/2"], + ["/home/1", "/home/2", "/home/3", "/home/4", "/home/5", "/home/6", "/home/7", "/home/8"], 10, ConnectionParams("localhost", 1883, 60), True @@ -55,8 +43,8 @@ class Worker(QObject, SubscriberCallback): self.error.emit() def onMessage(self, message: Message): - self.newMessage.emit(message.topic) - self.window.plot(message) + self.newMessage.emit(message) + # self.window.plot(message) def onCloseTopic(self, topic: str): pass @@ -75,12 +63,13 @@ class MainView(QDialog): self.dataIndex = 0 self.dataDict = {} - self.figure = plt.figure(figsize=([500,500])) + self.canvasDict = {} + self.figureDict = {} - self.canvas = FigureCanvas(self.figure) - self.toolbar = NavigationToolbar(self.canvas, self) + # self.toolbar = NavigationToolbar(self.canvas, self) - self.setMinimumSize(QSize(440, 240)) + # self.setMinimumSize(QSize(440, 240)) + self.setMinimumSize(QSize(1200, 800)) self.setWindowTitle("MQTT demo") # Add logger text field @@ -92,39 +81,62 @@ class MainView(QDialog): layout = QVBoxLayout() layout.addWidget(logger.widget) - layout.addWidget(self.toolbar) - layout.addWidget(self.canvas) + # layout.addWidget(self.toolbar) self.setLayout(layout) - self.initSubscriber() + scrollArea = QScrollArea(self) + scrollArea.setWidgetResizable(True) + scrollContent = QWidget() + self.grid = QGridLayout(scrollContent) + scrollArea.setWidget(scrollContent) + layout.addWidget(scrollArea) - def plot(self, message: Message): - self.figure.clear() + self.init_subscriber() + def plot(self, message: Message): if message.topic in self.dataDict: self.dataDict[message.topic].append(message.value) + + figure = self.figureDict[message.topic] + figure.clear() + + plt.figure(figure.number) + plt.plot(self.dataDict[message.topic]) + + self.canvasDict[message.topic].draw() else: self.dataDict[message.topic] = [message.value] - self.chartsNum += 1 - rows = math.ceil(self.chartsNum / 2) + figure = plt.figure(figsize=[500, 500]) + canvas = FigureCanvas(figure) + layout = QHBoxLayout() - b = 0 - for a in self.dataDict.values(): - self.figure.add_subplot(rows, 2, b + 1) - b += 1 - plt.plot(a) + plt.plot(self.dataDict[message.topic]) - self.canvas.draw() + self.canvasDict[message.topic] = canvas + self.figureDict[message.topic] = figure + + widget = QWidget() + widget.setLayout(layout) + button = QPushButton(':') + button.setFixedSize(QSize(40, 40)) + layout.addWidget(canvas) + layout.addWidget(button) + layout.setAlignment(button, QtCore.Qt.AlignTop) + widget.setMinimumSize(QSize(500, 500)) + + self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2) + + self.chartsNum += 1 - def initSubscriber(self): + def init_subscriber(self): self.workerThread = QThread() self.worker = Worker() self.worker.moveToThread(self.workerThread) self.workerThread.started.connect(self.worker.start) - # self.worker.newMessage.connect( - # lambda message: self.b.insertPlainText(message + "\n") - # ) + self.worker.newMessage.connect( + lambda message: self.plot(message) + ) self.worker.window = self self.workerThread.start() -- GitLab From be89639f4c19b9b39247f2990da1ef80d0950da5 Mon Sep 17 00:00:00 2001 From: Martin Forejt <mforejt@students.zcu.cz> Date: Mon, 24 May 2021 12:46:43 +0000 Subject: [PATCH 02/12] Feature/8921 preferences dialog --- .../service/mqtt/mqtt_subscriber.py | 11 +- aswi2021vochomurka/service/subscriber.py | 11 +- aswi2021vochomurka/view/main_view.py | 96 +++++++++++--- aswi2021vochomurka/view/settings.py | 120 ++++++++++++++++++ 4 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 aswi2021vochomurka/view/settings.py diff --git a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py index be7b979..fb07edb 100644 --- a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py +++ b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py @@ -7,6 +7,7 @@ from aswi2021vochomurka.service.subscriber import Subscriber class MQTTSubscriber(Subscriber): + client: mqtt.Client = None # The callback for when the client receives a CONNACK response from the server. def on_connect(self, client, userdata, flags, rc, properties=None): @@ -40,6 +41,7 @@ class MQTTSubscriber(Subscriber): def start(self): super().start() client = mqtt.Client() + self.client = client client.on_connect = self.on_connect client.on_message = self.on_message client.on_disconnect = self.on_disconnect @@ -47,7 +49,6 @@ class MQTTSubscriber(Subscriber): if not self.params.anonymous: logging.info('Using credentials, username=' + self.params.username + ', password=' + self.params.password) - client.tls_set() client.username_pw_set(self.params.username, self.params.password) try: @@ -63,3 +64,11 @@ class MQTTSubscriber(Subscriber): return client.loop_forever() + + def stop(self): + super().stop() + if self.client is not None: + logging.info("Disconnecting from broker") + client = self.client + self.client = None + client.disconnect() diff --git a/aswi2021vochomurka/service/subscriber.py b/aswi2021vochomurka/service/subscriber.py index 53c4fe1..25ce50b 100644 --- a/aswi2021vochomurka/service/subscriber.py +++ b/aswi2021vochomurka/service/subscriber.py @@ -1,19 +1,20 @@ import time +from typing import Dict from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.schedulers.base import STATE_STOPPED from aswi2021vochomurka.model.Message import Message from aswi2021vochomurka.service.file_manager import FileManager from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback from aswi2021vochomurka.service.subscriber_params import SubscriberParams -from typing import Dict class Subscriber: callback: SubscriberCallback params: SubscriberParams - scheduler = BackgroundScheduler() + scheduler: BackgroundScheduler files: Dict[str, FileManager] = {} def __init__(self, callback: SubscriberCallback, params: SubscriberParams): @@ -22,17 +23,19 @@ class Subscriber: def start(self): # start scheduler to check closed topics + self.scheduler = BackgroundScheduler() self.scheduler.add_job(self.check_closed_topics, 'interval', seconds=self.params.closeLimit) self.scheduler.start() def stop(self): - self.scheduler.shutdown() + if self.scheduler.state != STATE_STOPPED: + self.scheduler.shutdown() self.close_files() def close_files(self): for topic in self.files: self.files.get(topic).close() - self.files = {} + self.files.clear() def check_closed_topics(self): t = time.time() diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 0f154a2..8e34463 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -2,9 +2,10 @@ import logging import matplotlib.pyplot as plt from PyQt5 import QtCore -from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal -from PyQt5.QtWidgets import QDialog, QPushButton, QVBoxLayout -from PyQt5.QtWidgets import QHBoxLayout, QGridLayout, QScrollArea, QWidget +from PyQt5 import QtGui +from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal, QSettings +from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QGridLayout +from PyQt5.QtWidgets import QMenuBar, QAction, QPushButton from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from aswi2021vochomurka.model.Message import Message @@ -13,6 +14,8 @@ from aswi2021vochomurka.service.subscriber import Subscriber from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams from aswi2021vochomurka.view.logger_view import LoggerView +from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_KEEPALIVE, \ + DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS class Worker(QObject, SubscriberCallback): @@ -21,18 +24,19 @@ class Worker(QObject, SubscriberCallback): error = pyqtSignal(Exception) newMessage = pyqtSignal(Message) subscriber: Subscriber = None + params: SubscriberParams - params = SubscriberParams( - ["/home/1", "/home/2", "/home/3", "/home/4", "/home/5", "/home/6", "/home/7", "/home/8"], - 10, - ConnectionParams("localhost", 1883, 60), - True - ) + def __init__(self, params: SubscriberParams) -> None: + super().__init__() + self.params = params def start(self): self.subscriber = MQTTSubscriber(self, self.params) self.subscriber.start() + def stop(self): + self.subscriber.stop() + def onConnected(self): self.connected.emit() @@ -40,7 +44,7 @@ class Worker(QObject, SubscriberCallback): self.disconnected.emit() def onError(self): - self.error.emit() + pass def onMessage(self, message: Message): self.newMessage.emit(message) @@ -50,7 +54,7 @@ class Worker(QObject, SubscriberCallback): pass -class MainView(QDialog): +class MainView(QMainWindow): worker: Worker = None workerThread: QThread = None @@ -70,20 +74,17 @@ class MainView(QDialog): # self.setMinimumSize(QSize(440, 240)) self.setMinimumSize(QSize(1200, 800)) - self.setWindowTitle("MQTT demo") - - # Add logger text field - logger = LoggerView(self) - formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M') - logger.setFormatter(formatter) - logger.setLevel(logging.INFO) - logging.getLogger('').addHandler(logger) + self.setWindowTitle("MQTT client") + logger = self._createLoggerView() layout = QVBoxLayout() layout.addWidget(logger.widget) # layout.addWidget(self.toolbar) - self.setLayout(layout) + widget = QWidget() + widget.setLayout(layout) + self.setCentralWidget(widget) + self._createMenuBar() scrollArea = QScrollArea(self) scrollArea.setWidgetResizable(True) @@ -94,6 +95,21 @@ class MainView(QDialog): self.init_subscriber() + def _createLoggerView(self): + logger = LoggerView(self) + formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M') + logger.setFormatter(formatter) + logger.setLevel(logging.INFO) + logging.getLogger('').addHandler(logger) + return logger + + def _createMenuBar(self): + menuBar = QMenuBar(self) + settingsAction = QAction("&Settings", self) + settingsAction.triggered.connect(self.settings) + menuBar.addAction(settingsAction) + self.setMenuBar(menuBar) + def plot(self, message: Message): if message.topic in self.dataDict: self.dataDict[message.topic].append(message.value) @@ -130,9 +146,27 @@ class MainView(QDialog): self.chartsNum += 1 + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + self.worker.stop() + + def settings(self): + dialog = SettingsDialog() + if dialog.exec_(): + self.reconnect() + + def disconnect(self): + self.worker.stop() + self.workerThread.quit() + self.workerThread.wait() + + def reconnect(self): + self.disconnect() + self.worker.params = self.getConfigParams() + self.workerThread.start() + def init_subscriber(self): self.workerThread = QThread() - self.worker = Worker() + self.worker = Worker(self.getConfigParams()) self.worker.moveToThread(self.workerThread) self.workerThread.started.connect(self.worker.start) self.worker.newMessage.connect( @@ -140,3 +174,23 @@ class MainView(QDialog): ) self.worker.window = self self.workerThread.start() + + def getConfigParams(self) -> SubscriberParams: + settings = QSettings("Vochomurka", "MQTTClient") + + connection = ConnectionParams( + settings.value("connection_host", DEFAULT_HOST, str), + settings.value("connection_port", DEFAULT_PORT, int), + settings.value("connection_keepalive", DEFAULT_KEEPALIVE, int) + ) + + params = SubscriberParams( + settings.value("topics_items", DEFAULT_TOPICS), + settings.value("topics_timeout", DEFAULT_TIMEOUT), + connection, + settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool), + settings.value("connection_username", DEFAULT_USERNAME, str), + settings.value("connection_password", DEFAULT_USERNAME, str), + ) + + return params diff --git a/aswi2021vochomurka/view/settings.py b/aswi2021vochomurka/view/settings.py new file mode 100644 index 0000000..379d615 --- /dev/null +++ b/aswi2021vochomurka/view/settings.py @@ -0,0 +1,120 @@ +from PyQt5 import QtCore +from PyQt5.QtCore import QSettings, QSize +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox, QGroupBox, QFormLayout, QLabel, QLineEdit, QSpinBox, \ + QCheckBox, QPushButton, QListWidget, QListWidgetItem + +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_ANONYMOUS = True +DEFAULT_USERNAME = "" +DEFAULT_PASSWORD = "" +DEFAULT_TOPICS = ["/home/1", "/home/2"] +DEFAULT_TIMEOUT = 60 + + +class SettingsDialog(QDialog): + topics = DEFAULT_TOPICS + + def __init__(self): + super(SettingsDialog, self).__init__(None, + QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) + self.settings = QSettings("Vochomurka", "MQTTClient") + self.setWindowTitle("Settings") + self.setMinimumSize(QSize(600, 500)) + + connectionGroupBox = QGroupBox("Connection") + connectionLayout = QFormLayout() + self.hostInput = QLineEdit(self.settings.value("connection_host", DEFAULT_HOST, str)) + connectionLayout.addRow(QLabel("Host:"), self.hostInput) + self.portInput = QSpinBox() + self.portInput.setMaximum(65535) + self.portInput.setValue(self.settings.value("connection_port", DEFAULT_PORT, int)) + connectionLayout.addRow(QLabel("Port:"), self.portInput) + self.keepaliveInput = QSpinBox() + self.keepaliveInput.setMaximum(1000) + self.keepaliveInput.setValue(self.settings.value("connection_keepalive", DEFAULT_KEEPALIVE, int)) + connectionLayout.addRow(QLabel("Keepalive(s):"), self.keepaliveInput) + self.anonymousInput = QCheckBox() + self.anonymousInput.setChecked(self.settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool)) + self.anonymousInput.stateChanged.connect(self.anonymousChanged) + connectionLayout.addRow(QLabel("Anonymous:"), self.anonymousInput) + self.usernameInput = QLineEdit(self.settings.value("connection_username", DEFAULT_USERNAME, str)) + connectionLayout.addRow(QLabel("Username:"), self.usernameInput) + self.passwordInput = QLineEdit(self.settings.value("connection_password", DEFAULT_PASSWORD, str)) + connectionLayout.addRow(QLabel("Password:"), self.passwordInput) + self.anonymousChanged() + connectionGroupBox.setLayout(connectionLayout) + + topicsGroupBox = QGroupBox("Topics") + topicsLayout = QFormLayout() + + self.topics = self.settings.value("topics_items", DEFAULT_TOPICS, list) + self.topicsListWidget = QListWidget() + for topic in self.topics: + item = QListWidgetItem() + item.setText(topic) + item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) + self.topicsListWidget.addItem(item) + + topicsLayout.addRow(self.topicsListWidget) + add = QPushButton("Add") + add.setFixedWidth(60) + add.clicked.connect(self.addTopic) + remove = QPushButton("Remove") + remove.setFixedWidth(60) + remove.clicked.connect(self.removeTopic) + topicsLayout.addRow(add, remove) + + self.timeoutInput = QSpinBox() + self.timeoutInput.setMaximum(1000) + self.timeoutInput.setToolTip("Unsubscribe topic and close file when there is not new message after this " + "timeout (in seconds) expires") + timeoutLabel = QLabel("Topic timeout(s):") + timeoutLabel.setToolTip("Unsubscribe topic and close file when there is not new message after this " + "timeout (in seconds) expires") + self.timeoutInput.setValue(self.settings.value("topics_timeout", DEFAULT_TIMEOUT, int)) + topicsLayout.addRow(timeoutLabel, self.timeoutInput) + + topicsGroupBox.setLayout(topicsLayout) + + buttonBox = QDialogButtonBox() + buttonBox.addButton("Save and Reconnect", QDialogButtonBox.AcceptRole) + buttonBox.addButton("Cancel", QDialogButtonBox.RejectRole) + buttonBox.accepted.connect(self.accept) + buttonBox.rejected.connect(self.reject) + + mainLayout = QVBoxLayout() + mainLayout.addWidget(connectionGroupBox) + mainLayout.addWidget(topicsGroupBox) + mainLayout.addWidget(buttonBox) + self.setLayout(mainLayout) + + def addTopic(self): + item = QListWidgetItem() + item.setText("/topic") + item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) + self.topicsListWidget.addItem(item) + + def removeTopic(self): + for item in self.topicsListWidget.selectedItems(): + self.topicsListWidget.takeItem(self.topicsListWidget.row(item)) + + def anonymousChanged(self): + self.usernameInput.setEnabled(not self.anonymousInput.isChecked()) + self.passwordInput.setEnabled(not self.anonymousInput.isChecked()) + + def accept(self) -> None: + super().accept() + self.topics = [] + for index in range(self.topicsListWidget.count()): + self.topics.append(self.topicsListWidget.item(index).text()) + + self.settings.setValue("topics_items", self.topics) + self.settings.setValue("topics_timeout", self.timeoutInput.value()) + self.settings.setValue("connection_host", self.hostInput.text()) + self.settings.setValue("connection_port", self.portInput.value()) + self.settings.setValue("connection_keepalive", self.keepaliveInput.value()) + self.settings.setValue("connection_anonymous", self.anonymousInput.isChecked()) + self.settings.setValue("connection_username", self.usernameInput.text()) + self.settings.setValue("connection_password", self.passwordInput.text()) -- GitLab From f727f55f45ef74a412accabac04cdc7f9715d5d4 Mon Sep 17 00:00:00 2001 From: Martin Forejt <mforejt@students.zcu.cz> Date: Thu, 27 May 2021 21:56:15 +0000 Subject: [PATCH 03/12] Feature/8996 config file location --- aswi2021vochomurka/app.py | 11 +++++++++++ aswi2021vochomurka/settings.ini | 9 +++++++++ aswi2021vochomurka/view/main_view.py | 4 ++-- aswi2021vochomurka/view/settings.py | 7 ++++++- 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 aswi2021vochomurka/settings.ini diff --git a/aswi2021vochomurka/app.py b/aswi2021vochomurka/app.py index ea63143..a7ec8a6 100644 --- a/aswi2021vochomurka/app.py +++ b/aswi2021vochomurka/app.py @@ -1,5 +1,6 @@ import logging +from PyQt5.QtCore import QSettings, QCoreApplication from PyQt5.QtWidgets import QApplication from aswi2021vochomurka.view.main_view import MainView @@ -31,3 +32,13 @@ def init_logger(): logging.getLogger('apscheduler').setLevel(logging.WARNING) logging.getLogger('matplotlib').setLevel(logging.WARNING) + + +def init_settings(): + QCoreApplication.setOrganizationName('Vochomurka') + QCoreApplication.setOrganizationDomain('vochomurka.org') + QCoreApplication.setApplicationName('MQTTClient') + + QSettings.setDefaultFormat(QSettings.IniFormat) + QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, '.') + settings = QSettings() diff --git a/aswi2021vochomurka/settings.ini b/aswi2021vochomurka/settings.ini new file mode 100644 index 0000000..df13f88 --- /dev/null +++ b/aswi2021vochomurka/settings.ini @@ -0,0 +1,9 @@ +[General] +topics_items=/home/1, /home/2 +topics_timeout=60 +connection_host=localhost +connection_port=1883 +connection_keepalive=60 +connection_anonymous=true +connection_username= +connection_password= diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 8e34463..299b5eb 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -15,7 +15,7 @@ from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams from aswi2021vochomurka.view.logger_view import LoggerView from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_KEEPALIVE, \ - DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS + DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS, get_settings class Worker(QObject, SubscriberCallback): @@ -176,7 +176,7 @@ class MainView(QMainWindow): self.workerThread.start() def getConfigParams(self) -> SubscriberParams: - settings = QSettings("Vochomurka", "MQTTClient") + settings = get_settings() connection = ConnectionParams( settings.value("connection_host", DEFAULT_HOST, str), diff --git a/aswi2021vochomurka/view/settings.py b/aswi2021vochomurka/view/settings.py index 379d615..f76d693 100644 --- a/aswi2021vochomurka/view/settings.py +++ b/aswi2021vochomurka/view/settings.py @@ -13,13 +13,18 @@ DEFAULT_TOPICS = ["/home/1", "/home/2"] DEFAULT_TIMEOUT = 60 +def get_settings(): + settings = QSettings('settings.ini', QSettings.IniFormat) + return settings + + class SettingsDialog(QDialog): topics = DEFAULT_TOPICS def __init__(self): super(SettingsDialog, self).__init__(None, QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) - self.settings = QSettings("Vochomurka", "MQTTClient") + self.settings = get_settings() self.setWindowTitle("Settings") self.setMinimumSize(QSize(600, 500)) -- GitLab From 523382c19daeef1d2eeec6e3ec868b24b011d36f Mon Sep 17 00:00:00 2001 From: MFori <forejt.martin97@gmail.com> Date: Fri, 28 May 2021 00:21:07 +0200 Subject: [PATCH 04/12] Re: #8997 - refactoring, comments --- aswi2021vochomurka/model/Message.py | 3 ++ aswi2021vochomurka/service/file_manager.py | 24 +++++++++++++++- aswi2021vochomurka/service/message_parser.py | 9 ++++++ .../service/mqtt/mqtt_subscriber.py | 23 +++++++++++++-- aswi2021vochomurka/service/subscriber.py | 28 +++++++++++++++++++ .../service/subscriber_params.py | 6 ++++ 6 files changed, 90 insertions(+), 3 deletions(-) diff --git a/aswi2021vochomurka/model/Message.py b/aswi2021vochomurka/model/Message.py index 3e70281..9da2d52 100644 --- a/aswi2021vochomurka/model/Message.py +++ b/aswi2021vochomurka/model/Message.py @@ -2,6 +2,9 @@ from recordclass import RecordClass class Message(RecordClass): + """ + Message wrapper + """ topic: str index: int date: str diff --git a/aswi2021vochomurka/service/file_manager.py b/aswi2021vochomurka/service/file_manager.py index 7f29c4a..b9e2b29 100644 --- a/aswi2021vochomurka/service/file_manager.py +++ b/aswi2021vochomurka/service/file_manager.py @@ -15,17 +15,32 @@ trans = str.maketrans({ ".": "_"}) -def create_filename(message: Message): +def create_filename(message: Message) -> str: + """ + Create file name based on message data + :param message: message + :return: filename + """ name = "data/" + message.topic.translate(trans) + "/" + message.date + "_" + message.time + ".csv" return name class FileManager: + """ + Helper class for writing incoming message to files + Each topic has created own instance of this class + """ topic: str lastUpdate: float file: TextIO def __init__(self, topic: str, message: Message): + """ + Constructing new FileManager will create new file and write first message + :param topic: topic + :param message: message + :except when creating new file fails + """ self.topic = topic logging.debug('opening file ' + self.topic) @@ -39,10 +54,17 @@ class FileManager: self.write(message) def write(self, message: Message): + """ + Append message to file + :param message: message + """ self.file.write(message.date + ";" + message.time + ";" + str(message.index) + ";" + str(message.value) + "\n") self.lastUpdate = time.time() def close(self): + """ + Close file + """ logging.debug('closing file ' + self.topic) self.file.flush() self.file.close() diff --git a/aswi2021vochomurka/service/message_parser.py b/aswi2021vochomurka/service/message_parser.py index 7c9a81a..9787dd3 100644 --- a/aswi2021vochomurka/service/message_parser.py +++ b/aswi2021vochomurka/service/message_parser.py @@ -6,10 +6,19 @@ from aswi2021vochomurka.model.Message import Message class ParseException(Exception): + """ + May be throw when message has incorrect format + """ pass def parse_mqtt_message(message: MQTTMessage) -> Message: + """ + Parse MQTTMessage to Message + :param message: messsage + :return: message + :except: when message has incorrect format + """ data = message.payload.decode("utf-8") parts = data.split(";") logging.debug('Parsing message: ' + data + ', parts: ' + str(len(parts))) diff --git a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py index fb07edb..3466137 100644 --- a/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py +++ b/aswi2021vochomurka/service/mqtt/mqtt_subscriber.py @@ -7,10 +7,16 @@ from aswi2021vochomurka.service.subscriber import Subscriber class MQTTSubscriber(Subscriber): + """ + MQTT subscriber, implementation of Subscriber over MQTT protocol + """ client: mqtt.Client = None - # The callback for when the client receives a CONNACK response from the server. def on_connect(self, client, userdata, flags, rc, properties=None): + """ + The callback for when the client receives a CONNACK response from the server. + See: mqtt.Client.on_connect for info about params + """ logging.info('Connected with result code ' + str(rc)) self.callback.onConnected() @@ -20,8 +26,11 @@ class MQTTSubscriber(Subscriber): logging.info('Subscribed to topic: ' + topic) client.subscribe(topic) - # The callback for when a PUBLISH message is received from the server. def on_message(self, client, userdata, message: mqtt.MQTTMessage): + """ + The callback for when a PUBLISH message is received from the server. + See: mqtt.Client.on_message for info about params + """ try: m = parse_mqtt_message(message) logging.info('Message: ' + str(m)) @@ -34,11 +43,18 @@ class MQTTSubscriber(Subscriber): pass def on_disconnect(self, client, userdata, rc): + """ + The callback for when the client disconnects from the server. + See: mqtt.Client.on_disconnect for info about params + """ logging.info('Disconnected') self.callback.onDisconnected() self.stop() def start(self): + """ + Start mqtt client + """ super().start() client = mqtt.Client() self.client = client @@ -66,6 +82,9 @@ class MQTTSubscriber(Subscriber): client.loop_forever() def stop(self): + """ + Stop mqtt client + """ super().stop() if self.client is not None: logging.info("Disconnecting from broker") diff --git a/aswi2021vochomurka/service/subscriber.py b/aswi2021vochomurka/service/subscriber.py index 25ce50b..82ad578 100644 --- a/aswi2021vochomurka/service/subscriber.py +++ b/aswi2021vochomurka/service/subscriber.py @@ -11,6 +11,11 @@ from aswi2021vochomurka.service.subscriber_params import SubscriberParams class Subscriber: + """ + Subscriber is responsible for establishing communication with broker and notifying + about new message via callback + Subscriber must be started via 'start' method and stopped via 'stop' method + """ callback: SubscriberCallback params: SubscriberParams @@ -18,26 +23,43 @@ class Subscriber: files: Dict[str, FileManager] = {} def __init__(self, callback: SubscriberCallback, params: SubscriberParams): + """ + Constructor + :param callback: callback + :param params: params + """ self.callback = callback self.params = params def start(self): + """ + Start subscriber + """ # start scheduler to check closed topics self.scheduler = BackgroundScheduler() self.scheduler.add_job(self.check_closed_topics, 'interval', seconds=self.params.closeLimit) self.scheduler.start() def stop(self): + """ + Stop subscriber + """ if self.scheduler.state != STATE_STOPPED: self.scheduler.shutdown() self.close_files() def close_files(self): + """ + Close all open files + """ for topic in self.files: self.files.get(topic).close() self.files.clear() def check_closed_topics(self): + """ + May be called periodically for checking for expired timeout for closing topic + """ t = time.time() for topic in list(self.files): file = self.files.get(topic) @@ -47,8 +69,14 @@ class Subscriber: self.files.pop(topic) def write_to_file(self, message: Message): + """ + Write message to file + :param message: message + """ if message.topic in self.files: + # file exist, just append message self.files.get(message.topic).write(message) else: + # new message for this topic, create new file fm = FileManager(message.topic, message) self.files[message.topic] = fm diff --git a/aswi2021vochomurka/service/subscriber_params.py b/aswi2021vochomurka/service/subscriber_params.py index 7aaa227..e9af434 100644 --- a/aswi2021vochomurka/service/subscriber_params.py +++ b/aswi2021vochomurka/service/subscriber_params.py @@ -3,12 +3,18 @@ from typing import List class ConnectionParams(RecordClass): + """ + Connection params to connect to broker + """ host: str port: int timeout: int class SubscriberParams(RecordClass): + """ + Params for Subscriber + """ # list of topics to subscribe topics: List[str] # close limit in seconds -- GitLab From 7d3e8585324d5c530b84069844f10c8157350781 Mon Sep 17 00:00:00 2001 From: MFori <forejt.martin97@gmail.com> Date: Fri, 28 May 2021 00:40:14 +0200 Subject: [PATCH 05/12] fix: load timeout from ini file as int --- aswi2021vochomurka/app.py | 6 +----- aswi2021vochomurka/view/main_view.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/aswi2021vochomurka/app.py b/aswi2021vochomurka/app.py index a7ec8a6..6068b0c 100644 --- a/aswi2021vochomurka/app.py +++ b/aswi2021vochomurka/app.py @@ -9,6 +9,7 @@ from aswi2021vochomurka.view.main_view import MainView class Application(QApplication): def __init__(self, sys_argv): init_logger() + init_settings() super(Application, self).__init__(sys_argv) logging.info('App started') self.main_view = MainView() @@ -35,10 +36,5 @@ def init_logger(): def init_settings(): - QCoreApplication.setOrganizationName('Vochomurka') - QCoreApplication.setOrganizationDomain('vochomurka.org') - QCoreApplication.setApplicationName('MQTTClient') - QSettings.setDefaultFormat(QSettings.IniFormat) QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, '.') - settings = QSettings() diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 299b5eb..173ff2a 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -186,7 +186,7 @@ class MainView(QMainWindow): params = SubscriberParams( settings.value("topics_items", DEFAULT_TOPICS), - settings.value("topics_timeout", DEFAULT_TIMEOUT), + settings.value("topics_timeout", DEFAULT_TIMEOUT, int), connection, settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool), settings.value("connection_username", DEFAULT_USERNAME, str), -- GitLab From c9bff745ffd43995c77a48e3032d0ea19ef241cb Mon Sep 17 00:00:00 2001 From: Jan Rach <rachj@students.zcu.cz> Date: Fri, 28 May 2021 11:15:06 +0000 Subject: [PATCH 06/12] Feature/9017 graph title --- aswi2021vochomurka/view/main_view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 173ff2a..6792e70 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -117,7 +117,8 @@ class MainView(QMainWindow): figure = self.figureDict[message.topic] figure.clear() - plt.figure(figure.number) + figure = plt.figure(figure.number) + figure.suptitle(message.topic) plt.plot(self.dataDict[message.topic]) self.canvasDict[message.topic].draw() @@ -129,6 +130,7 @@ class MainView(QMainWindow): layout = QHBoxLayout() plt.plot(self.dataDict[message.topic]) + figure.suptitle(message.topic) self.canvasDict[message.topic] = canvas self.figureDict[message.topic] = figure -- GitLab From ff2d1b99de302ce51512a8a3a3f5a8154bc48ee1 Mon Sep 17 00:00:00 2001 From: Jan Rach <rachj@students.zcu.cz> Date: Wed, 2 Jun 2021 14:46:54 +0200 Subject: [PATCH 07/12] Re: #9016 - Topic close plot reaction implemented --- aswi2021vochomurka/view/main_view.py | 45 +++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 8e34463..76e9388 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -23,6 +23,7 @@ class Worker(QObject, SubscriberCallback): disconnected = pyqtSignal() error = pyqtSignal(Exception) newMessage = pyqtSignal(Message) + closeTopic = pyqtSignal(str) subscriber: Subscriber = None params: SubscriberParams @@ -48,10 +49,11 @@ class Worker(QObject, SubscriberCallback): def onMessage(self, message: Message): self.newMessage.emit(message) - # self.window.plot(message) def onCloseTopic(self, topic: str): - pass + print("Close topic") + self.closeTopic.emit(topic) + #pass class MainView(QMainWindow): @@ -66,9 +68,11 @@ class MainView(QMainWindow): self.dataIndex = 0 self.dataDict = {} - self.canvasDict = {} self.figureDict = {} + self.widgetDict = {} + + self.widgetList = [] # self.toolbar = NavigationToolbar(self.canvas, self) @@ -126,7 +130,7 @@ class MainView(QMainWindow): figure = plt.figure(figsize=[500, 500]) canvas = FigureCanvas(figure) - layout = QHBoxLayout() + self.layout = QHBoxLayout() plt.plot(self.dataDict[message.topic]) @@ -134,18 +138,40 @@ class MainView(QMainWindow): self.figureDict[message.topic] = figure widget = QWidget() - widget.setLayout(layout) + self.widgetDict[message.topic] = widget + self.widgetList.append(widget) + widget.setLayout(self.layout) button = QPushButton(':') button.setFixedSize(QSize(40, 40)) - layout.addWidget(canvas) - layout.addWidget(button) - layout.setAlignment(button, QtCore.Qt.AlignTop) + self.layout.addWidget(canvas) + self.layout.addWidget(button) + self.layout.setAlignment(button, QtCore.Qt.AlignTop) widget.setMinimumSize(QSize(500, 500)) self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2) self.chartsNum += 1 + def deletePlot(self, topic: str): + widget = self.widgetDict[topic] + self.widgetList.remove(widget) + widget.setParent(None) + + del self.widgetDict[topic] + del self.canvasDict[topic] + del self.figureDict[topic] + del self.dataDict[topic] + + self.reorganizePlots() + + def reorganizePlots(self): + count = 0 + for widget in self.widgetList: + self.grid.addWidget(widget, int(count / 2), count % 2) + count += 1 + + self.chartsNum -= 1 + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: self.worker.stop() @@ -172,6 +198,9 @@ class MainView(QMainWindow): self.worker.newMessage.connect( lambda message: self.plot(message) ) + self.worker.closeTopic.connect( + lambda topic: self.deletePlot(topic) + ) self.worker.window = self self.workerThread.start() -- GitLab From e402bc719aaff0467c1f4e3cd7ff9ae3efc4e6ac Mon Sep 17 00:00:00 2001 From: Pavel <murglm@seznam.cz> Date: Thu, 3 Jun 2021 11:57:31 +0200 Subject: [PATCH 08/12] Re: #8998 - refactoring, comments --- aswi2021vochomurka/view/logger_view.py | 14 +++++ aswi2021vochomurka/view/main_view.py | 77 +++++++++++++++++++++++--- aswi2021vochomurka/view/settings.py | 19 +++++++ 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/aswi2021vochomurka/view/logger_view.py b/aswi2021vochomurka/view/logger_view.py index 2ed4231..0649bc2 100644 --- a/aswi2021vochomurka/view/logger_view.py +++ b/aswi2021vochomurka/view/logger_view.py @@ -5,9 +5,15 @@ from PyQt5.QtWidgets import QPlainTextEdit class LoggerView(logging.Handler, QObject): + """ + LoggerView represents console in gui application. + """ append = pyqtSignal(str) def __init__(self, parent): + """ + Constructor + """ super().__init__() super(QObject, self).__init__() @@ -19,10 +25,18 @@ class LoggerView(logging.Handler, QObject): ) def emit(self, record): + """ + Emit message from record + :param record: record + """ msg = self.format(record) self.append.emit(msg) def appendMessage(self, msg): + """ + Append message + :param msg: message + """ self.widget.appendPlainText(msg) self.widget.verticalScrollBar().setValue(self.widget.verticalScrollBar().maximum()) diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 4b86d1b..227444e 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -19,6 +19,9 @@ from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAU class Worker(QObject, SubscriberCallback): + """ + Worker representing thread + """ connected = pyqtSignal() disconnected = pyqtSignal() error = pyqtSignal(Exception) @@ -28,62 +31,83 @@ class Worker(QObject, SubscriberCallback): params: SubscriberParams def __init__(self, params: SubscriberParams) -> None: + """ + Constructor + """ super().__init__() self.params = params def start(self): + """ + Start worker + """ self.subscriber = MQTTSubscriber(self, self.params) self.subscriber.start() def stop(self): + """ + Stop worker + """ self.subscriber.stop() def onConnected(self): + """ + Emit connection signal + """ self.connected.emit() def onDisconnected(self): + """ + Emit disconnection signal + """ self.disconnected.emit() def onError(self): pass def onMessage(self, message: Message): + """ + Emit message signal + :param message: message + """ self.newMessage.emit(message) def onCloseTopic(self, topic: str): + """ + Emit close topic signal + :param topic: topic + """ print("Close topic") self.closeTopic.emit(topic) - #pass class MainView(QMainWindow): + """ + Main window of application. + Displays all parts of the application. + """ worker: Worker = None workerThread: QThread = None def __init__(self): + """ + Constructor - displays all parts of the application. + """ super(MainView, self).__init__() self.chartsNum = 0 - self.arrayData = [] - - self.dataIndex = 0 self.dataDict = {} self.canvasDict = {} self.figureDict = {} self.widgetDict = {} - self.widgetList = [] - # self.toolbar = NavigationToolbar(self.canvas, self) - - # self.setMinimumSize(QSize(440, 240)) self.setMinimumSize(QSize(1200, 800)) self.setWindowTitle("MQTT client") logger = self._createLoggerView() layout = QVBoxLayout() layout.addWidget(logger.widget) - # layout.addWidget(self.toolbar) widget = QWidget() widget.setLayout(layout) @@ -100,6 +124,9 @@ class MainView(QMainWindow): self.init_subscriber() def _createLoggerView(self): + """ + Create logger view + """ logger = LoggerView(self) formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M') logger.setFormatter(formatter) @@ -108,6 +135,9 @@ class MainView(QMainWindow): return logger def _createMenuBar(self): + """ + Creates menu bar + """ menuBar = QMenuBar(self) settingsAction = QAction("&Settings", self) settingsAction.triggered.connect(self.settings) @@ -115,7 +145,12 @@ class MainView(QMainWindow): self.setMenuBar(menuBar) def plot(self, message: Message): + """ + Plots new charts or updates old ones + :param message: message + """ if message.topic in self.dataDict: + # topic already exists self.dataDict[message.topic].append(message.value) figure = self.figureDict[message.topic] @@ -127,6 +162,7 @@ class MainView(QMainWindow): self.canvasDict[message.topic].draw() else: + # new topic self.dataDict[message.topic] = [message.value] figure = plt.figure(figsize=[500, 500]) @@ -155,6 +191,10 @@ class MainView(QMainWindow): self.chartsNum += 1 def deletePlot(self, topic: str): + """ + Deletes plot + :param topic: topic + """ widget = self.widgetDict[topic] self.widgetList.remove(widget) widget.setParent(None) @@ -167,6 +207,9 @@ class MainView(QMainWindow): self.reorganizePlots() def reorganizePlots(self): + """ + Reorganize plots + """ count = 0 for widget in self.widgetList: self.grid.addWidget(widget, int(count / 2), count % 2) @@ -178,21 +221,33 @@ class MainView(QMainWindow): self.worker.stop() def settings(self): + """ + Opens settings dialog + """ dialog = SettingsDialog() if dialog.exec_(): self.reconnect() def disconnect(self): + """ + Disconnect + """ self.worker.stop() self.workerThread.quit() self.workerThread.wait() def reconnect(self): + """ + Reconnect + """ self.disconnect() self.worker.params = self.getConfigParams() self.workerThread.start() def init_subscriber(self): + """ + Initialization of subscriber + """ self.workerThread = QThread() self.worker = Worker(self.getConfigParams()) self.worker.moveToThread(self.workerThread) @@ -207,6 +262,10 @@ class MainView(QMainWindow): self.workerThread.start() def getConfigParams(self) -> SubscriberParams: + """ + Returns config parameters + :return: config parameters + """ settings = get_settings() connection = ConnectionParams( diff --git a/aswi2021vochomurka/view/settings.py b/aswi2021vochomurka/view/settings.py index f76d693..e6e2153 100644 --- a/aswi2021vochomurka/view/settings.py +++ b/aswi2021vochomurka/view/settings.py @@ -19,9 +19,16 @@ def get_settings(): class SettingsDialog(QDialog): + """ + Settings dialog. + In settings dialog is possible to change settings of application. + """ topics = DEFAULT_TOPICS def __init__(self): + """ + Constructor + """ super(SettingsDialog, self).__init__(None, QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) self.settings = get_settings() @@ -96,20 +103,32 @@ class SettingsDialog(QDialog): self.setLayout(mainLayout) def addTopic(self): + """ + Add topic + """ item = QListWidgetItem() item.setText("/topic") item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) self.topicsListWidget.addItem(item) def removeTopic(self): + """ + Remove topic + """ for item in self.topicsListWidget.selectedItems(): self.topicsListWidget.takeItem(self.topicsListWidget.row(item)) def anonymousChanged(self): + """ + Changing anonymous/user status + """ self.usernameInput.setEnabled(not self.anonymousInput.isChecked()) self.passwordInput.setEnabled(not self.anonymousInput.isChecked()) def accept(self) -> None: + """ + Accept changes + """ super().accept() self.topics = [] for index in range(self.topicsListWidget.count()): -- GitLab From 4e32720c141076f723413d7cd05cb2a69e8500fb Mon Sep 17 00:00:00 2001 From: Jan Rach <rachj@students.zcu.cz> Date: Thu, 3 Jun 2021 17:15:42 +0200 Subject: [PATCH 09/12] Re: #8968 - second function plotting implemented --- aswi2021vochomurka/view/main_view.py | 38 ++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 227444e..011abdb 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -4,7 +4,7 @@ import matplotlib.pyplot as plt from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal, QSettings -from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QGridLayout +from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QGridLayout, QFileDialog from PyQt5.QtWidgets import QMenuBar, QAction, QPushButton from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas @@ -97,6 +97,7 @@ class MainView(QMainWindow): self.chartsNum = 0 self.dataDict = {} + self.dataDict2 = {} self.canvasDict = {} self.figureDict = {} self.widgetDict = {} @@ -160,6 +161,9 @@ class MainView(QMainWindow): figure.suptitle(message.topic) plt.plot(self.dataDict[message.topic]) + if message.topic in self.dataDict2: + plt.plot(self.dataDict2[message.topic]) + self.canvasDict[message.topic].draw() else: # new topic @@ -179,8 +183,10 @@ class MainView(QMainWindow): self.widgetDict[message.topic] = widget self.widgetList.append(widget) widget.setLayout(self.layout) - button = QPushButton(':') + button = QPushButton('Load') + button.clicked.connect(lambda: self.getFile(message.topic)) button.setFixedSize(QSize(40, 40)) + self.layout.addWidget(canvas) self.layout.addWidget(button) self.layout.setAlignment(button, QtCore.Qt.AlignTop) @@ -190,6 +196,34 @@ class MainView(QMainWindow): self.chartsNum += 1 + def getFile(self, topic: str): + fname = QFileDialog.getOpenFileName(self, 'Open file', + 'c:\\', "CSV files (*.csv)") + try: + figure = self.figureDict[topic] + figure.clear() + + self.dataDict2[topic] = [] + + file1 = open(fname[0], 'r') + lines = file1.readlines() + + count = 0 + for line in lines: + count += 1 + parts = line.split(';') + value = float(parts[3]) + self.dataDict2[topic].append(value) + + figure = plt.figure(figure.number) + figure.suptitle(topic) + plt.plot(self.dataDict[topic]) + plt.plot(self.dataDict2[topic]) + + self.canvasDict[topic].draw() + except: + print("Loading error") + def deletePlot(self, topic: str): """ Deletes plot -- GitLab From e91c91d86b4845707091cc46a0c722af65ae91e2 Mon Sep 17 00:00:00 2001 From: Rach <jrach@GK-DOMAIN> Date: Thu, 3 Jun 2021 17:30:20 +0200 Subject: [PATCH 10/12] Re: #8968 - logging error --- aswi2021vochomurka/view/main_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index 011abdb..c2db28c 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -222,7 +222,7 @@ class MainView(QMainWindow): self.canvasDict[topic].draw() except: - print("Loading error") + logging.error("Error while loading and displaying data file") def deletePlot(self, topic: str): """ -- GitLab From e89316f60206baffc2982c4fc5833a96b8e65fcc Mon Sep 17 00:00:00 2001 From: Jan Rach <rachj@students.zcu.cz> Date: Fri, 4 Jun 2021 09:56:20 +0200 Subject: [PATCH 11/12] Re: #8968 - second function delete feature implemented. --- aswi2021vochomurka/view/main_view.py | 40 ++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/aswi2021vochomurka/view/main_view.py b/aswi2021vochomurka/view/main_view.py index c2db28c..a296e50 100644 --- a/aswi2021vochomurka/view/main_view.py +++ b/aswi2021vochomurka/view/main_view.py @@ -170,10 +170,12 @@ class MainView(QMainWindow): self.dataDict[message.topic] = [message.value] figure = plt.figure(figsize=[500, 500]) + canvas = FigureCanvas(figure) self.layout = QHBoxLayout() plt.plot(self.dataDict[message.topic]) + figure.suptitle(message.topic) self.canvasDict[message.topic] = canvas @@ -187,9 +189,18 @@ class MainView(QMainWindow): button.clicked.connect(lambda: self.getFile(message.topic)) button.setFixedSize(QSize(40, 40)) + button2 = QPushButton('Del') + button2.clicked.connect(lambda: self.deleteSecond(message.topic)) + button2.setFixedSize(QSize(40, 40)) + self.layout.addWidget(canvas) - self.layout.addWidget(button) - self.layout.setAlignment(button, QtCore.Qt.AlignTop) + + boxLayout = QVBoxLayout() + boxLayout.addWidget(button) + boxLayout.addWidget(button2) + boxLayout.setAlignment(button, QtCore.Qt.AlignTop) + boxLayout.setAlignment(button2, QtCore.Qt.AlignTop) + self.layout.addLayout(boxLayout) widget.setMinimumSize(QSize(500, 500)) self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2) @@ -224,6 +235,19 @@ class MainView(QMainWindow): except: logging.error("Error while loading and displaying data file") + def deleteSecond(self, topic: str): + if topic in self.dataDict2: + del self.dataDict2[topic] + + figure = self.figureDict[topic] + figure.clear() + + figure = plt.figure(figure.number) + figure.suptitle(topic) + plt.plot(self.dataDict[topic]) + + self.canvasDict[topic].draw() + def deletePlot(self, topic: str): """ Deletes plot @@ -237,6 +261,8 @@ class MainView(QMainWindow): del self.canvasDict[topic] del self.figureDict[topic] del self.dataDict[topic] + if topic in self.dataDict2: + del self.dataDict2[topic] self.reorganizePlots() @@ -274,6 +300,16 @@ class MainView(QMainWindow): """ Reconnect """ + for widget in self.widgetDict.values(): + widget.setParent(None) + + self.widgetDict.clear() + self.widgetList.clear() + self.canvasDict.clear() + self.figureDict.clear() + self.dataDict.clear() + self.dataDict2.clear() + self.disconnect() self.worker.params = self.getConfigParams() self.workerThread.start() -- GitLab From 068e18695fc34545ed1996ebcbb38d609d32c36f Mon Sep 17 00:00:00 2001 From: Martin Forejt <mforejt@students.zcu.cz> Date: Fri, 4 Jun 2021 11:26:38 +0000 Subject: [PATCH 12/12] Feature/build --- README.md | 17 +++++++++++++++++ aswi2021vochomurka/app.py | 4 ++++ pyproject.toml | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b2010c..109b679 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ # Konfigurovatelný dashboard zobrazovánà senzorových dat (KIV) - Vochomůrka + +# Build +``` +poetry install +poetry build +``` +```aswi2021vochomurka-1.0.0-py3-none-any.whl``` deployable file will be generated at dist folder + +# Install +``` +pip install aswi2021vochomurka-1.0.0-py3-none-any.whl +``` + +# Run +``` +python -m aswi2021vochomurka.main +``` \ No newline at end of file diff --git a/aswi2021vochomurka/app.py b/aswi2021vochomurka/app.py index 6068b0c..f495985 100644 --- a/aswi2021vochomurka/app.py +++ b/aswi2021vochomurka/app.py @@ -1,4 +1,5 @@ import logging +import os from PyQt5.QtCore import QSettings, QCoreApplication from PyQt5.QtWidgets import QApplication @@ -17,6 +18,9 @@ class Application(QApplication): def init_logger(): + if not os.path.exists('data'): + os.mkdir('data') + logging.basicConfig( level=logging.DEBUG, filename='data/app.log', diff --git a/pyproject.toml b/pyproject.toml index 34d46a8..7171e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aswi2021vochomurka" -version = "0.1.0" +version = "1.0.0" description = "" authors = ["Tym Vochomurka"] -- GitLab