Revize 64ca87d0
Přidáno uživatelem Martin Forejt před téměř 4 roky(ů)
README.md | ||
---|---|---|
1 | 1 |
# Konfigurovatelný dashboard zobrazování senzorových dat (KIV) - Vochomůrka |
2 |
|
|
3 |
# Build |
|
4 |
``` |
|
5 |
poetry install |
|
6 |
poetry build |
|
7 |
``` |
|
8 |
```aswi2021vochomurka-1.0.0-py3-none-any.whl``` deployable file will be generated at dist folder |
|
9 |
|
|
10 |
# Install |
|
11 |
``` |
|
12 |
pip install aswi2021vochomurka-1.0.0-py3-none-any.whl |
|
13 |
``` |
|
14 |
|
|
15 |
# Run |
|
16 |
``` |
|
17 |
python -m aswi2021vochomurka.main |
|
18 |
``` |
aswi2021vochomurka/app.py | ||
---|---|---|
1 | 1 |
import logging |
2 |
import os |
|
2 | 3 |
|
4 |
from PyQt5.QtCore import QSettings, QCoreApplication |
|
3 | 5 |
from PyQt5.QtWidgets import QApplication |
4 | 6 |
|
5 | 7 |
from aswi2021vochomurka.view.main_view import MainView |
... | ... | |
8 | 10 |
class Application(QApplication): |
9 | 11 |
def __init__(self, sys_argv): |
10 | 12 |
init_logger() |
13 |
init_settings() |
|
11 | 14 |
super(Application, self).__init__(sys_argv) |
12 | 15 |
logging.info('App started') |
13 | 16 |
self.main_view = MainView() |
... | ... | |
15 | 18 |
|
16 | 19 |
|
17 | 20 |
def init_logger(): |
21 |
if not os.path.exists('data'): |
|
22 |
os.mkdir('data') |
|
23 |
|
|
18 | 24 |
logging.basicConfig( |
19 | 25 |
level=logging.DEBUG, |
20 | 26 |
filename='data/app.log', |
... | ... | |
31 | 37 |
|
32 | 38 |
logging.getLogger('apscheduler').setLevel(logging.WARNING) |
33 | 39 |
logging.getLogger('matplotlib').setLevel(logging.WARNING) |
40 |
|
|
41 |
|
|
42 |
def init_settings(): |
|
43 |
QSettings.setDefaultFormat(QSettings.IniFormat) |
|
44 |
QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, '.') |
aswi2021vochomurka/model/Message.py | ||
---|---|---|
2 | 2 |
|
3 | 3 |
|
4 | 4 |
class Message(RecordClass): |
5 |
""" |
|
6 |
Message wrapper |
|
7 |
""" |
|
5 | 8 |
topic: str |
6 | 9 |
index: int |
7 | 10 |
date: str |
aswi2021vochomurka/service/file_manager.py | ||
---|---|---|
15 | 15 |
".": "_"}) |
16 | 16 |
|
17 | 17 |
|
18 |
def create_filename(message: Message): |
|
18 |
def create_filename(message: Message) -> str: |
|
19 |
""" |
|
20 |
Create file name based on message data |
|
21 |
:param message: message |
|
22 |
:return: filename |
|
23 |
""" |
|
19 | 24 |
name = "data/" + message.topic.translate(trans) + "/" + message.date + "_" + message.time + ".csv" |
20 | 25 |
return name |
21 | 26 |
|
22 | 27 |
|
23 | 28 |
class FileManager: |
29 |
""" |
|
30 |
Helper class for writing incoming message to files |
|
31 |
Each topic has created own instance of this class |
|
32 |
""" |
|
24 | 33 |
topic: str |
25 | 34 |
lastUpdate: float |
26 | 35 |
file: TextIO |
27 | 36 |
|
28 | 37 |
def __init__(self, topic: str, message: Message): |
38 |
""" |
|
39 |
Constructing new FileManager will create new file and write first message |
|
40 |
:param topic: topic |
|
41 |
:param message: message |
|
42 |
:except when creating new file fails |
|
43 |
""" |
|
29 | 44 |
self.topic = topic |
30 | 45 |
logging.debug('opening file ' + self.topic) |
31 | 46 |
|
... | ... | |
39 | 54 |
self.write(message) |
40 | 55 |
|
41 | 56 |
def write(self, message: Message): |
57 |
""" |
|
58 |
Append message to file |
|
59 |
:param message: message |
|
60 |
""" |
|
42 | 61 |
self.file.write(message.date + ";" + message.time + ";" + str(message.index) + ";" + str(message.value) + "\n") |
43 | 62 |
self.lastUpdate = time.time() |
44 | 63 |
|
45 | 64 |
def close(self): |
65 |
""" |
|
66 |
Close file |
|
67 |
""" |
|
46 | 68 |
logging.debug('closing file ' + self.topic) |
47 | 69 |
self.file.flush() |
48 | 70 |
self.file.close() |
aswi2021vochomurka/service/message_parser.py | ||
---|---|---|
6 | 6 |
|
7 | 7 |
|
8 | 8 |
class ParseException(Exception): |
9 |
""" |
|
10 |
May be throw when message has incorrect format |
|
11 |
""" |
|
9 | 12 |
pass |
10 | 13 |
|
11 | 14 |
|
12 | 15 |
def parse_mqtt_message(message: MQTTMessage) -> Message: |
16 |
""" |
|
17 |
Parse MQTTMessage to Message |
|
18 |
:param message: messsage |
|
19 |
:return: message |
|
20 |
:except: when message has incorrect format |
|
21 |
""" |
|
13 | 22 |
data = message.payload.decode("utf-8") |
14 | 23 |
parts = data.split(";") |
15 | 24 |
logging.debug('Parsing message: ' + data + ', parts: ' + str(len(parts))) |
aswi2021vochomurka/service/mqtt/mqtt_subscriber.py | ||
---|---|---|
7 | 7 |
|
8 | 8 |
|
9 | 9 |
class MQTTSubscriber(Subscriber): |
10 |
""" |
|
11 |
MQTT subscriber, implementation of Subscriber over MQTT protocol |
|
12 |
""" |
|
13 |
client: mqtt.Client = None |
|
10 | 14 |
|
11 |
# The callback for when the client receives a CONNACK response from the server. |
|
12 | 15 |
def on_connect(self, client, userdata, flags, rc, properties=None): |
16 |
""" |
|
17 |
The callback for when the client receives a CONNACK response from the server. |
|
18 |
See: mqtt.Client.on_connect for info about params |
|
19 |
""" |
|
13 | 20 |
logging.info('Connected with result code ' + str(rc)) |
14 | 21 |
self.callback.onConnected() |
15 | 22 |
|
... | ... | |
19 | 26 |
logging.info('Subscribed to topic: ' + topic) |
20 | 27 |
client.subscribe(topic) |
21 | 28 |
|
22 |
# The callback for when a PUBLISH message is received from the server. |
|
23 | 29 |
def on_message(self, client, userdata, message: mqtt.MQTTMessage): |
30 |
""" |
|
31 |
The callback for when a PUBLISH message is received from the server. |
|
32 |
See: mqtt.Client.on_message for info about params |
|
33 |
""" |
|
24 | 34 |
try: |
25 | 35 |
m = parse_mqtt_message(message) |
26 | 36 |
logging.info('Message: ' + str(m)) |
... | ... | |
33 | 43 |
pass |
34 | 44 |
|
35 | 45 |
def on_disconnect(self, client, userdata, rc): |
46 |
""" |
|
47 |
The callback for when the client disconnects from the server. |
|
48 |
See: mqtt.Client.on_disconnect for info about params |
|
49 |
""" |
|
36 | 50 |
logging.info('Disconnected') |
37 | 51 |
self.callback.onDisconnected() |
38 | 52 |
self.stop() |
39 | 53 |
|
40 | 54 |
def start(self): |
55 |
""" |
|
56 |
Start mqtt client |
|
57 |
""" |
|
41 | 58 |
super().start() |
42 | 59 |
client = mqtt.Client() |
60 |
self.client = client |
|
43 | 61 |
client.on_connect = self.on_connect |
44 | 62 |
client.on_message = self.on_message |
45 | 63 |
client.on_disconnect = self.on_disconnect |
... | ... | |
47 | 65 |
|
48 | 66 |
if not self.params.anonymous: |
49 | 67 |
logging.info('Using credentials, username=' + self.params.username + ', password=' + self.params.password) |
50 |
client.tls_set() |
|
51 | 68 |
client.username_pw_set(self.params.username, self.params.password) |
52 | 69 |
|
53 | 70 |
try: |
... | ... | |
63 | 80 |
return |
64 | 81 |
|
65 | 82 |
client.loop_forever() |
83 |
|
|
84 |
def stop(self): |
|
85 |
""" |
|
86 |
Stop mqtt client |
|
87 |
""" |
|
88 |
super().stop() |
|
89 |
if self.client is not None: |
|
90 |
logging.info("Disconnecting from broker") |
|
91 |
client = self.client |
|
92 |
self.client = None |
|
93 |
client.disconnect() |
aswi2021vochomurka/service/subscriber.py | ||
---|---|---|
1 | 1 |
import time |
2 |
from typing import Dict |
|
2 | 3 |
|
3 | 4 |
from apscheduler.schedulers.background import BackgroundScheduler |
5 |
from apscheduler.schedulers.base import STATE_STOPPED |
|
4 | 6 |
|
5 | 7 |
from aswi2021vochomurka.model.Message import Message |
6 | 8 |
from aswi2021vochomurka.service.file_manager import FileManager |
7 | 9 |
from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback |
8 | 10 |
from aswi2021vochomurka.service.subscriber_params import SubscriberParams |
9 |
from typing import Dict |
|
10 | 11 |
|
11 | 12 |
|
12 | 13 |
class Subscriber: |
14 |
""" |
|
15 |
Subscriber is responsible for establishing communication with broker and notifying |
|
16 |
about new message via callback |
|
17 |
Subscriber must be started via 'start' method and stopped via 'stop' method |
|
18 |
""" |
|
13 | 19 |
callback: SubscriberCallback |
14 | 20 |
params: SubscriberParams |
15 | 21 |
|
16 |
scheduler = BackgroundScheduler()
|
|
22 |
scheduler: BackgroundScheduler
|
|
17 | 23 |
files: Dict[str, FileManager] = {} |
18 | 24 |
|
19 | 25 |
def __init__(self, callback: SubscriberCallback, params: SubscriberParams): |
26 |
""" |
|
27 |
Constructor |
|
28 |
:param callback: callback |
|
29 |
:param params: params |
|
30 |
""" |
|
20 | 31 |
self.callback = callback |
21 | 32 |
self.params = params |
22 | 33 |
|
23 | 34 |
def start(self): |
35 |
""" |
|
36 |
Start subscriber |
|
37 |
""" |
|
24 | 38 |
# start scheduler to check closed topics |
39 |
self.scheduler = BackgroundScheduler() |
|
25 | 40 |
self.scheduler.add_job(self.check_closed_topics, 'interval', seconds=self.params.closeLimit) |
26 | 41 |
self.scheduler.start() |
27 | 42 |
|
28 | 43 |
def stop(self): |
29 |
self.scheduler.shutdown() |
|
44 |
""" |
|
45 |
Stop subscriber |
|
46 |
""" |
|
47 |
if self.scheduler.state != STATE_STOPPED: |
|
48 |
self.scheduler.shutdown() |
|
30 | 49 |
self.close_files() |
31 | 50 |
|
32 | 51 |
def close_files(self): |
52 |
""" |
|
53 |
Close all open files |
|
54 |
""" |
|
33 | 55 |
for topic in self.files: |
34 | 56 |
self.files.get(topic).close() |
35 |
self.files = {}
|
|
57 |
self.files.clear()
|
|
36 | 58 |
|
37 | 59 |
def check_closed_topics(self): |
60 |
""" |
|
61 |
May be called periodically for checking for expired timeout for closing topic |
|
62 |
""" |
|
38 | 63 |
t = time.time() |
39 | 64 |
for topic in list(self.files): |
40 | 65 |
file = self.files.get(topic) |
... | ... | |
44 | 69 |
self.files.pop(topic) |
45 | 70 |
|
46 | 71 |
def write_to_file(self, message: Message): |
72 |
""" |
|
73 |
Write message to file |
|
74 |
:param message: message |
|
75 |
""" |
|
47 | 76 |
if message.topic in self.files: |
77 |
# file exist, just append message |
|
48 | 78 |
self.files.get(message.topic).write(message) |
49 | 79 |
else: |
80 |
# new message for this topic, create new file |
|
50 | 81 |
fm = FileManager(message.topic, message) |
51 | 82 |
self.files[message.topic] = fm |
aswi2021vochomurka/service/subscriber_params.py | ||
---|---|---|
3 | 3 |
|
4 | 4 |
|
5 | 5 |
class ConnectionParams(RecordClass): |
6 |
""" |
|
7 |
Connection params to connect to broker |
|
8 |
""" |
|
6 | 9 |
host: str |
7 | 10 |
port: int |
8 | 11 |
timeout: int |
9 | 12 |
|
10 | 13 |
|
11 | 14 |
class SubscriberParams(RecordClass): |
15 |
""" |
|
16 |
Params for Subscriber |
|
17 |
""" |
|
12 | 18 |
# list of topics to subscribe |
13 | 19 |
topics: List[str] |
14 | 20 |
# close limit in seconds |
aswi2021vochomurka/settings.ini | ||
---|---|---|
1 |
[General] |
|
2 |
topics_items=/home/1, /home/2 |
|
3 |
topics_timeout=60 |
|
4 |
connection_host=localhost |
|
5 |
connection_port=1883 |
|
6 |
connection_keepalive=60 |
|
7 |
connection_anonymous=true |
|
8 |
connection_username= |
|
9 |
connection_password= |
aswi2021vochomurka/view/logger_view.py | ||
---|---|---|
5 | 5 |
|
6 | 6 |
|
7 | 7 |
class LoggerView(logging.Handler, QObject): |
8 |
""" |
|
9 |
LoggerView represents console in gui application. |
|
10 |
""" |
|
8 | 11 |
append = pyqtSignal(str) |
9 | 12 |
|
10 | 13 |
def __init__(self, parent): |
14 |
""" |
|
15 |
Constructor |
|
16 |
""" |
|
11 | 17 |
super().__init__() |
12 | 18 |
super(QObject, self).__init__() |
13 | 19 |
|
... | ... | |
19 | 25 |
) |
20 | 26 |
|
21 | 27 |
def emit(self, record): |
28 |
""" |
|
29 |
Emit message from record |
|
30 |
:param record: record |
|
31 |
""" |
|
22 | 32 |
msg = self.format(record) |
23 | 33 |
self.append.emit(msg) |
24 | 34 |
|
25 | 35 |
def appendMessage(self, msg): |
36 |
""" |
|
37 |
Append message |
|
38 |
:param msg: message |
|
39 |
""" |
|
26 | 40 |
self.widget.appendPlainText(msg) |
27 | 41 |
self.widget.verticalScrollBar().setValue(self.widget.verticalScrollBar().maximum()) |
28 | 42 |
|
aswi2021vochomurka/view/main_view.py | ||
---|---|---|
1 | 1 |
import logging |
2 |
import math |
|
3 |
import random |
|
4 | 2 |
|
5 |
from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal |
|
6 |
from PyQt5.QtWidgets import QMainWindow, QPlainTextEdit, QDialog, QHBoxLayout |
|
7 |
from numpy import pi, sin, cos, tan, exp |
|
8 |
from matplotlib.pyplot import subplot |
|
3 |
import matplotlib.pyplot as plt |
|
4 |
from PyQt5 import QtCore |
|
5 |
from PyQt5 import QtGui |
|
6 |
from PyQt5.QtCore import QSize, QThread, QObject, pyqtSignal, QSettings |
|
7 |
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QGridLayout, QFileDialog |
|
8 |
from PyQt5.QtWidgets import QMenuBar, QAction, QPushButton |
|
9 |
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas |
|
9 | 10 |
|
10 | 11 |
from aswi2021vochomurka.model.Message import Message |
11 | 12 |
from aswi2021vochomurka.service.mqtt.mqtt_subscriber import MQTTSubscriber |
12 | 13 |
from aswi2021vochomurka.service.subscriber import Subscriber |
13 | 14 |
from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback |
14 | 15 |
from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams |
15 |
|
|
16 |
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas |
|
17 |
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar |
|
18 |
import matplotlib.pyplot as plt |
|
19 |
|
|
20 |
import sys |
|
21 |
from PyQt5.QtWidgets import QDialog, QApplication, QPushButton, QVBoxLayout |
|
22 |
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas |
|
23 |
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar |
|
24 |
import matplotlib.pyplot as plt |
|
25 |
import random |
|
26 |
|
|
27 | 16 |
from aswi2021vochomurka.view.logger_view import LoggerView |
17 |
from aswi2021vochomurka.view.settings import SettingsDialog, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_KEEPALIVE, \ |
|
18 |
DEFAULT_ANONYMOUS, DEFAULT_USERNAME, DEFAULT_TIMEOUT, DEFAULT_TOPICS, get_settings |
|
28 | 19 |
|
29 | 20 |
|
30 | 21 |
class Worker(QObject, SubscriberCallback): |
22 |
""" |
|
23 |
Worker representing thread |
|
24 |
""" |
|
31 | 25 |
connected = pyqtSignal() |
32 | 26 |
disconnected = pyqtSignal() |
33 | 27 |
error = pyqtSignal(Exception) |
34 |
newMessage = pyqtSignal(str) |
|
28 |
newMessage = pyqtSignal(Message) |
|
29 |
closeTopic = pyqtSignal(str) |
|
35 | 30 |
subscriber: Subscriber = None |
31 |
params: SubscriberParams |
|
36 | 32 |
|
37 |
params = SubscriberParams(
|
|
38 |
["/home/1", "/home/2"],
|
|
39 |
10,
|
|
40 |
ConnectionParams("localhost", 1883, 60),
|
|
41 |
True
|
|
42 |
)
|
|
33 |
def __init__(self, params: SubscriberParams) -> None:
|
|
34 |
"""
|
|
35 |
Constructor
|
|
36 |
"""
|
|
37 |
super().__init__()
|
|
38 |
self.params = params
|
|
43 | 39 |
|
44 | 40 |
def start(self): |
41 |
""" |
|
42 |
Start worker |
|
43 |
""" |
|
45 | 44 |
self.subscriber = MQTTSubscriber(self, self.params) |
46 | 45 |
self.subscriber.start() |
47 | 46 |
|
47 |
def stop(self): |
|
48 |
""" |
|
49 |
Stop worker |
|
50 |
""" |
|
51 |
self.subscriber.stop() |
|
52 |
|
|
48 | 53 |
def onConnected(self): |
54 |
""" |
|
55 |
Emit connection signal |
|
56 |
""" |
|
49 | 57 |
self.connected.emit() |
50 | 58 |
|
51 | 59 |
def onDisconnected(self): |
60 |
""" |
|
61 |
Emit disconnection signal |
|
62 |
""" |
|
52 | 63 |
self.disconnected.emit() |
53 | 64 |
|
54 | 65 |
def onError(self): |
55 |
self.error.emit()
|
|
66 |
pass
|
|
56 | 67 |
|
57 | 68 |
def onMessage(self, message: Message): |
58 |
self.newMessage.emit(message.topic) |
|
59 |
self.window.plot(message) |
|
69 |
""" |
|
70 |
Emit message signal |
|
71 |
:param message: message |
|
72 |
""" |
|
73 |
self.newMessage.emit(message) |
|
60 | 74 |
|
61 | 75 |
def onCloseTopic(self, topic: str): |
62 |
pass |
|
63 |
|
|
64 |
|
|
65 |
class MainView(QDialog): |
|
76 |
""" |
|
77 |
Emit close topic signal |
|
78 |
:param topic: topic |
|
79 |
""" |
|
80 |
print("Close topic") |
|
81 |
self.closeTopic.emit(topic) |
|
82 |
|
|
83 |
|
|
84 |
class MainView(QMainWindow): |
|
85 |
""" |
|
86 |
Main window of application. |
|
87 |
Displays all parts of the application. |
|
88 |
""" |
|
66 | 89 |
worker: Worker = None |
67 | 90 |
workerThread: QThread = None |
68 | 91 |
|
69 | 92 |
def __init__(self): |
93 |
""" |
|
94 |
Constructor - displays all parts of the application. |
|
95 |
""" |
|
70 | 96 |
super(MainView, self).__init__() |
71 | 97 |
|
72 | 98 |
self.chartsNum = 0 |
73 |
self.arrayData = [] |
|
74 |
|
|
75 |
self.dataIndex = 0 |
|
76 | 99 |
self.dataDict = {} |
100 |
self.dataDict2 = {} |
|
101 |
self.canvasDict = {} |
|
102 |
self.figureDict = {} |
|
103 |
self.widgetDict = {} |
|
104 |
self.widgetList = [] |
|
77 | 105 |
|
78 |
self.figure = plt.figure(figsize=([500,500])) |
|
106 |
self.setMinimumSize(QSize(1200, 800)) |
|
107 |
self.setWindowTitle("MQTT client") |
|
79 | 108 |
|
80 |
self.canvas = FigureCanvas(self.figure) |
|
81 |
self.toolbar = NavigationToolbar(self.canvas, self) |
|
109 |
logger = self._createLoggerView() |
|
110 |
layout = QVBoxLayout() |
|
111 |
layout.addWidget(logger.widget) |
|
112 |
|
|
113 |
widget = QWidget() |
|
114 |
widget.setLayout(layout) |
|
115 |
self.setCentralWidget(widget) |
|
116 |
self._createMenuBar() |
|
82 | 117 |
|
83 |
self.setMinimumSize(QSize(440, 240)) |
|
84 |
self.setWindowTitle("MQTT demo") |
|
118 |
scrollArea = QScrollArea(self) |
|
119 |
scrollArea.setWidgetResizable(True) |
|
120 |
scrollContent = QWidget() |
|
121 |
self.grid = QGridLayout(scrollContent) |
|
122 |
scrollArea.setWidget(scrollContent) |
|
123 |
layout.addWidget(scrollArea) |
|
85 | 124 |
|
86 |
# Add logger text field |
|
125 |
self.init_subscriber() |
|
126 |
|
|
127 |
def _createLoggerView(self): |
|
128 |
""" |
|
129 |
Create logger view |
|
130 |
""" |
|
87 | 131 |
logger = LoggerView(self) |
88 | 132 |
formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M') |
89 | 133 |
logger.setFormatter(formatter) |
90 | 134 |
logger.setLevel(logging.INFO) |
91 | 135 |
logging.getLogger('').addHandler(logger) |
136 |
return logger |
|
137 |
|
|
138 |
def _createMenuBar(self): |
|
139 |
""" |
|
140 |
Creates menu bar |
|
141 |
""" |
|
142 |
menuBar = QMenuBar(self) |
|
143 |
settingsAction = QAction("&Settings", self) |
|
144 |
settingsAction.triggered.connect(self.settings) |
|
145 |
menuBar.addAction(settingsAction) |
|
146 |
self.setMenuBar(menuBar) |
|
92 | 147 |
|
93 |
layout = QVBoxLayout() |
|
94 |
layout.addWidget(logger.widget) |
|
95 |
layout.addWidget(self.toolbar) |
|
96 |
layout.addWidget(self.canvas) |
|
148 |
def plot(self, message: Message): |
|
149 |
""" |
|
150 |
Plots new charts or updates old ones |
|
151 |
:param message: message |
|
152 |
""" |
|
153 |
if message.topic in self.dataDict: |
|
154 |
# topic already exists |
|
155 |
self.dataDict[message.topic].append(message.value) |
|
97 | 156 |
|
98 |
self.setLayout(layout) |
|
157 |
figure = self.figureDict[message.topic] |
|
158 |
figure.clear() |
|
99 | 159 |
|
100 |
self.initSubscriber() |
|
160 |
figure = plt.figure(figure.number) |
|
161 |
figure.suptitle(message.topic) |
|
162 |
plt.plot(self.dataDict[message.topic]) |
|
101 | 163 |
|
102 |
def plot(self, message: Message):
|
|
103 |
self.figure.clear()
|
|
164 |
if message.topic in self.dataDict2:
|
|
165 |
plt.plot(self.dataDict2[message.topic])
|
|
104 | 166 |
|
105 |
if message.topic in self.dataDict: |
|
106 |
self.dataDict[message.topic].append(message.value) |
|
167 |
self.canvasDict[message.topic].draw() |
|
107 | 168 |
else: |
169 |
# new topic |
|
108 | 170 |
self.dataDict[message.topic] = [message.value] |
109 |
self.chartsNum += 1 |
|
110 | 171 |
|
111 |
rows = math.ceil(self.chartsNum / 2) |
|
172 |
figure = plt.figure(figsize=[500, 500]) |
|
173 |
|
|
174 |
canvas = FigureCanvas(figure) |
|
175 |
self.layout = QHBoxLayout() |
|
176 |
|
|
177 |
plt.plot(self.dataDict[message.topic]) |
|
112 | 178 |
|
113 |
b = 0 |
|
114 |
for a in self.dataDict.values(): |
|
115 |
self.figure.add_subplot(rows, 2, b + 1) |
|
116 |
b += 1 |
|
117 |
plt.plot(a) |
|
179 |
figure.suptitle(message.topic) |
|
118 | 180 |
|
119 |
self.canvas.draw() |
|
181 |
self.canvasDict[message.topic] = canvas |
|
182 |
self.figureDict[message.topic] = figure |
|
120 | 183 |
|
121 |
def initSubscriber(self): |
|
184 |
widget = QWidget() |
|
185 |
self.widgetDict[message.topic] = widget |
|
186 |
self.widgetList.append(widget) |
|
187 |
widget.setLayout(self.layout) |
|
188 |
button = QPushButton('Load') |
|
189 |
button.clicked.connect(lambda: self.getFile(message.topic)) |
|
190 |
button.setFixedSize(QSize(40, 40)) |
|
191 |
|
|
192 |
button2 = QPushButton('Del') |
|
193 |
button2.clicked.connect(lambda: self.deleteSecond(message.topic)) |
|
194 |
button2.setFixedSize(QSize(40, 40)) |
|
195 |
|
|
196 |
self.layout.addWidget(canvas) |
|
197 |
|
|
198 |
boxLayout = QVBoxLayout() |
|
199 |
boxLayout.addWidget(button) |
|
200 |
boxLayout.addWidget(button2) |
|
201 |
boxLayout.setAlignment(button, QtCore.Qt.AlignTop) |
|
202 |
boxLayout.setAlignment(button2, QtCore.Qt.AlignTop) |
|
203 |
self.layout.addLayout(boxLayout) |
|
204 |
widget.setMinimumSize(QSize(500, 500)) |
|
205 |
|
|
206 |
self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2) |
|
207 |
|
|
208 |
self.chartsNum += 1 |
|
209 |
|
|
210 |
def getFile(self, topic: str): |
|
211 |
fname = QFileDialog.getOpenFileName(self, 'Open file', |
|
212 |
'c:\\', "CSV files (*.csv)") |
|
213 |
try: |
|
214 |
figure = self.figureDict[topic] |
|
215 |
figure.clear() |
|
216 |
|
|
217 |
self.dataDict2[topic] = [] |
|
218 |
|
|
219 |
file1 = open(fname[0], 'r') |
|
220 |
lines = file1.readlines() |
|
221 |
|
|
222 |
count = 0 |
|
223 |
for line in lines: |
|
224 |
count += 1 |
|
225 |
parts = line.split(';') |
|
226 |
value = float(parts[3]) |
|
227 |
self.dataDict2[topic].append(value) |
|
228 |
|
|
229 |
figure = plt.figure(figure.number) |
|
230 |
figure.suptitle(topic) |
|
231 |
plt.plot(self.dataDict[topic]) |
|
232 |
plt.plot(self.dataDict2[topic]) |
|
233 |
|
|
234 |
self.canvasDict[topic].draw() |
|
235 |
except: |
|
236 |
logging.error("Error while loading and displaying data file") |
|
237 |
|
|
238 |
def deleteSecond(self, topic: str): |
|
239 |
if topic in self.dataDict2: |
|
240 |
del self.dataDict2[topic] |
|
241 |
|
|
242 |
figure = self.figureDict[topic] |
|
243 |
figure.clear() |
|
244 |
|
|
245 |
figure = plt.figure(figure.number) |
|
246 |
figure.suptitle(topic) |
|
247 |
plt.plot(self.dataDict[topic]) |
|
248 |
|
|
249 |
self.canvasDict[topic].draw() |
|
250 |
|
|
251 |
def deletePlot(self, topic: str): |
|
252 |
""" |
|
253 |
Deletes plot |
|
254 |
:param topic: topic |
|
255 |
""" |
|
256 |
widget = self.widgetDict[topic] |
|
257 |
self.widgetList.remove(widget) |
|
258 |
widget.setParent(None) |
|
259 |
|
|
260 |
del self.widgetDict[topic] |
|
261 |
del self.canvasDict[topic] |
|
262 |
del self.figureDict[topic] |
|
263 |
del self.dataDict[topic] |
|
264 |
if topic in self.dataDict2: |
|
265 |
del self.dataDict2[topic] |
|
266 |
|
|
267 |
self.reorganizePlots() |
|
268 |
|
|
269 |
def reorganizePlots(self): |
|
270 |
""" |
|
271 |
Reorganize plots |
|
272 |
""" |
|
273 |
count = 0 |
|
274 |
for widget in self.widgetList: |
|
275 |
self.grid.addWidget(widget, int(count / 2), count % 2) |
|
276 |
count += 1 |
|
277 |
|
|
278 |
self.chartsNum -= 1 |
|
279 |
|
|
280 |
def closeEvent(self, a0: QtGui.QCloseEvent) -> None: |
|
281 |
self.worker.stop() |
|
282 |
|
|
283 |
def settings(self): |
|
284 |
""" |
|
285 |
Opens settings dialog |
|
286 |
""" |
|
287 |
dialog = SettingsDialog() |
|
288 |
if dialog.exec_(): |
|
289 |
self.reconnect() |
|
290 |
|
|
291 |
def disconnect(self): |
|
292 |
""" |
|
293 |
Disconnect |
|
294 |
""" |
|
295 |
self.worker.stop() |
|
296 |
self.workerThread.quit() |
|
297 |
self.workerThread.wait() |
|
298 |
|
|
299 |
def reconnect(self): |
|
300 |
""" |
|
301 |
Reconnect |
|
302 |
""" |
|
303 |
for widget in self.widgetDict.values(): |
|
304 |
widget.setParent(None) |
|
305 |
|
|
306 |
self.widgetDict.clear() |
|
307 |
self.widgetList.clear() |
|
308 |
self.canvasDict.clear() |
|
309 |
self.figureDict.clear() |
|
310 |
self.dataDict.clear() |
|
311 |
self.dataDict2.clear() |
|
312 |
|
|
313 |
self.disconnect() |
|
314 |
self.worker.params = self.getConfigParams() |
|
315 |
self.workerThread.start() |
|
316 |
|
|
317 |
def init_subscriber(self): |
|
318 |
""" |
|
319 |
Initialization of subscriber |
|
320 |
""" |
|
122 | 321 |
self.workerThread = QThread() |
123 |
self.worker = Worker() |
|
322 |
self.worker = Worker(self.getConfigParams())
|
|
124 | 323 |
self.worker.moveToThread(self.workerThread) |
125 | 324 |
self.workerThread.started.connect(self.worker.start) |
126 |
# self.worker.newMessage.connect( |
|
127 |
# lambda message: self.b.insertPlainText(message + "\n") |
|
128 |
# ) |
|
325 |
self.worker.newMessage.connect( |
|
326 |
lambda message: self.plot(message) |
|
327 |
) |
|
328 |
self.worker.closeTopic.connect( |
|
329 |
lambda topic: self.deletePlot(topic) |
|
330 |
) |
|
129 | 331 |
self.worker.window = self |
130 | 332 |
self.workerThread.start() |
333 |
|
|
334 |
def getConfigParams(self) -> SubscriberParams: |
|
335 |
""" |
|
336 |
Returns config parameters |
|
337 |
:return: config parameters |
|
338 |
""" |
|
339 |
settings = get_settings() |
|
340 |
|
|
341 |
connection = ConnectionParams( |
|
342 |
settings.value("connection_host", DEFAULT_HOST, str), |
|
343 |
settings.value("connection_port", DEFAULT_PORT, int), |
|
344 |
settings.value("connection_keepalive", DEFAULT_KEEPALIVE, int) |
|
345 |
) |
|
346 |
|
|
347 |
params = SubscriberParams( |
|
348 |
settings.value("topics_items", DEFAULT_TOPICS), |
|
349 |
settings.value("topics_timeout", DEFAULT_TIMEOUT, int), |
|
350 |
connection, |
|
351 |
settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool), |
|
352 |
settings.value("connection_username", DEFAULT_USERNAME, str), |
|
353 |
settings.value("connection_password", DEFAULT_USERNAME, str), |
|
354 |
) |
|
355 |
|
|
356 |
return params |
aswi2021vochomurka/view/settings.py | ||
---|---|---|
1 |
from PyQt5 import QtCore |
|
2 |
from PyQt5.QtCore import QSettings, QSize |
|
3 |
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox, QGroupBox, QFormLayout, QLabel, QLineEdit, QSpinBox, \ |
|
4 |
QCheckBox, QPushButton, QListWidget, QListWidgetItem |
|
5 |
|
|
6 |
DEFAULT_HOST = "localhost" |
|
7 |
DEFAULT_PORT = 1883 |
|
8 |
DEFAULT_KEEPALIVE = 60 |
|
9 |
DEFAULT_ANONYMOUS = True |
|
10 |
DEFAULT_USERNAME = "" |
|
11 |
DEFAULT_PASSWORD = "" |
|
12 |
DEFAULT_TOPICS = ["/home/1", "/home/2"] |
|
13 |
DEFAULT_TIMEOUT = 60 |
|
14 |
|
|
15 |
|
|
16 |
def get_settings(): |
|
17 |
settings = QSettings('settings.ini', QSettings.IniFormat) |
|
18 |
return settings |
|
19 |
|
|
20 |
|
|
21 |
class SettingsDialog(QDialog): |
|
22 |
""" |
|
23 |
Settings dialog. |
|
24 |
In settings dialog is possible to change settings of application. |
|
25 |
""" |
|
26 |
topics = DEFAULT_TOPICS |
|
27 |
|
|
28 |
def __init__(self): |
|
29 |
""" |
|
30 |
Constructor |
|
31 |
""" |
|
32 |
super(SettingsDialog, self).__init__(None, |
|
33 |
QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) |
|
34 |
self.settings = get_settings() |
|
35 |
self.setWindowTitle("Settings") |
|
36 |
self.setMinimumSize(QSize(600, 500)) |
|
37 |
|
|
38 |
connectionGroupBox = QGroupBox("Connection") |
|
39 |
connectionLayout = QFormLayout() |
|
40 |
self.hostInput = QLineEdit(self.settings.value("connection_host", DEFAULT_HOST, str)) |
|
41 |
connectionLayout.addRow(QLabel("Host:"), self.hostInput) |
|
42 |
self.portInput = QSpinBox() |
|
43 |
self.portInput.setMaximum(65535) |
|
44 |
self.portInput.setValue(self.settings.value("connection_port", DEFAULT_PORT, int)) |
|
45 |
connectionLayout.addRow(QLabel("Port:"), self.portInput) |
|
46 |
self.keepaliveInput = QSpinBox() |
|
47 |
self.keepaliveInput.setMaximum(1000) |
|
48 |
self.keepaliveInput.setValue(self.settings.value("connection_keepalive", DEFAULT_KEEPALIVE, int)) |
|
49 |
connectionLayout.addRow(QLabel("Keepalive(s):"), self.keepaliveInput) |
|
50 |
self.anonymousInput = QCheckBox() |
|
51 |
self.anonymousInput.setChecked(self.settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool)) |
|
52 |
self.anonymousInput.stateChanged.connect(self.anonymousChanged) |
|
53 |
connectionLayout.addRow(QLabel("Anonymous:"), self.anonymousInput) |
|
54 |
self.usernameInput = QLineEdit(self.settings.value("connection_username", DEFAULT_USERNAME, str)) |
|
55 |
connectionLayout.addRow(QLabel("Username:"), self.usernameInput) |
|
56 |
self.passwordInput = QLineEdit(self.settings.value("connection_password", DEFAULT_PASSWORD, str)) |
|
57 |
connectionLayout.addRow(QLabel("Password:"), self.passwordInput) |
|
58 |
self.anonymousChanged() |
|
59 |
connectionGroupBox.setLayout(connectionLayout) |
|
60 |
|
|
61 |
topicsGroupBox = QGroupBox("Topics") |
|
62 |
topicsLayout = QFormLayout() |
|
63 |
|
|
64 |
self.topics = self.settings.value("topics_items", DEFAULT_TOPICS, list) |
|
65 |
self.topicsListWidget = QListWidget() |
|
66 |
for topic in self.topics: |
|
67 |
item = QListWidgetItem() |
|
68 |
item.setText(topic) |
|
69 |
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) |
|
70 |
self.topicsListWidget.addItem(item) |
|
71 |
|
|
72 |
topicsLayout.addRow(self.topicsListWidget) |
|
73 |
add = QPushButton("Add") |
|
74 |
add.setFixedWidth(60) |
|
75 |
add.clicked.connect(self.addTopic) |
|
76 |
remove = QPushButton("Remove") |
|
77 |
remove.setFixedWidth(60) |
|
78 |
remove.clicked.connect(self.removeTopic) |
|
79 |
topicsLayout.addRow(add, remove) |
|
80 |
|
|
81 |
self.timeoutInput = QSpinBox() |
|
82 |
self.timeoutInput.setMaximum(1000) |
|
83 |
self.timeoutInput.setToolTip("Unsubscribe topic and close file when there is not new message after this " |
|
84 |
"timeout (in seconds) expires") |
|
85 |
timeoutLabel = QLabel("Topic timeout(s):") |
|
86 |
timeoutLabel.setToolTip("Unsubscribe topic and close file when there is not new message after this " |
|
87 |
"timeout (in seconds) expires") |
|
88 |
self.timeoutInput.setValue(self.settings.value("topics_timeout", DEFAULT_TIMEOUT, int)) |
|
89 |
topicsLayout.addRow(timeoutLabel, self.timeoutInput) |
|
90 |
|
|
91 |
topicsGroupBox.setLayout(topicsLayout) |
|
92 |
|
|
93 |
buttonBox = QDialogButtonBox() |
|
94 |
buttonBox.addButton("Save and Reconnect", QDialogButtonBox.AcceptRole) |
|
95 |
buttonBox.addButton("Cancel", QDialogButtonBox.RejectRole) |
|
96 |
buttonBox.accepted.connect(self.accept) |
|
97 |
buttonBox.rejected.connect(self.reject) |
|
98 |
|
|
99 |
mainLayout = QVBoxLayout() |
|
100 |
mainLayout.addWidget(connectionGroupBox) |
|
101 |
mainLayout.addWidget(topicsGroupBox) |
|
102 |
mainLayout.addWidget(buttonBox) |
|
103 |
self.setLayout(mainLayout) |
|
104 |
|
|
105 |
def addTopic(self): |
|
106 |
""" |
|
107 |
Add topic |
|
108 |
""" |
|
109 |
item = QListWidgetItem() |
|
110 |
item.setText("/topic") |
|
111 |
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) |
|
112 |
self.topicsListWidget.addItem(item) |
|
113 |
|
|
114 |
def removeTopic(self): |
|
115 |
""" |
|
116 |
Remove topic |
|
117 |
""" |
|
118 |
for item in self.topicsListWidget.selectedItems(): |
|
119 |
self.topicsListWidget.takeItem(self.topicsListWidget.row(item)) |
|
120 |
|
|
121 |
def anonymousChanged(self): |
|
122 |
""" |
|
123 |
Changing anonymous/user status |
|
124 |
""" |
|
125 |
self.usernameInput.setEnabled(not self.anonymousInput.isChecked()) |
|
126 |
self.passwordInput.setEnabled(not self.anonymousInput.isChecked()) |
|
127 |
|
|
128 |
def accept(self) -> None: |
|
129 |
""" |
|
130 |
Accept changes |
|
131 |
""" |
|
132 |
super().accept() |
|
133 |
self.topics = [] |
|
134 |
for index in range(self.topicsListWidget.count()): |
|
135 |
self.topics.append(self.topicsListWidget.item(index).text()) |
|
136 |
|
|
137 |
self.settings.setValue("topics_items", self.topics) |
|
138 |
self.settings.setValue("topics_timeout", self.timeoutInput.value()) |
|
139 |
self.settings.setValue("connection_host", self.hostInput.text()) |
|
140 |
self.settings.setValue("connection_port", self.portInput.value()) |
|
141 |
self.settings.setValue("connection_keepalive", self.keepaliveInput.value()) |
|
142 |
self.settings.setValue("connection_anonymous", self.anonymousInput.isChecked()) |
|
143 |
self.settings.setValue("connection_username", self.usernameInput.text()) |
|
144 |
self.settings.setValue("connection_password", self.passwordInput.text()) |
pyproject.toml | ||
---|---|---|
1 | 1 |
[tool.poetry] |
2 | 2 |
name = "aswi2021vochomurka" |
3 |
version = "0.1.0"
|
|
3 |
version = "1.0.0"
|
|
4 | 4 |
description = "" |
5 | 5 |
authors = ["Tym Vochomurka"] |
6 | 6 |
|
Také k dispozici: Unified diff
Develop