1
|
import logging
|
2
|
|
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
|
8
|
from PyQt5.QtWidgets import QMenuBar, QAction, QPushButton
|
9
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
10
|
|
11
|
from aswi2021vochomurka.model.Message import Message
|
12
|
from aswi2021vochomurka.service.mqtt.mqtt_subscriber import MQTTSubscriber
|
13
|
from aswi2021vochomurka.service.subscriber import Subscriber
|
14
|
from aswi2021vochomurka.service.subscriber_callback import SubscriberCallback
|
15
|
from aswi2021vochomurka.service.subscriber_params import SubscriberParams, ConnectionParams
|
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
|
19
|
|
20
|
|
21
|
class Worker(QObject, SubscriberCallback):
|
22
|
"""
|
23
|
Worker representing thread
|
24
|
"""
|
25
|
connected = pyqtSignal()
|
26
|
disconnected = pyqtSignal()
|
27
|
error = pyqtSignal(Exception)
|
28
|
newMessage = pyqtSignal(Message)
|
29
|
closeTopic = pyqtSignal(str)
|
30
|
subscriber: Subscriber = None
|
31
|
params: SubscriberParams
|
32
|
|
33
|
def __init__(self, params: SubscriberParams) -> None:
|
34
|
"""
|
35
|
Constructor
|
36
|
"""
|
37
|
super().__init__()
|
38
|
self.params = params
|
39
|
|
40
|
def start(self):
|
41
|
"""
|
42
|
Start worker
|
43
|
"""
|
44
|
self.subscriber = MQTTSubscriber(self, self.params)
|
45
|
self.subscriber.start()
|
46
|
|
47
|
def stop(self):
|
48
|
"""
|
49
|
Stop worker
|
50
|
"""
|
51
|
self.subscriber.stop()
|
52
|
|
53
|
def onConnected(self):
|
54
|
"""
|
55
|
Emit connection signal
|
56
|
"""
|
57
|
self.connected.emit()
|
58
|
|
59
|
def onDisconnected(self):
|
60
|
"""
|
61
|
Emit disconnection signal
|
62
|
"""
|
63
|
self.disconnected.emit()
|
64
|
|
65
|
def onError(self):
|
66
|
pass
|
67
|
|
68
|
def onMessage(self, message: Message):
|
69
|
"""
|
70
|
Emit message signal
|
71
|
:param message: message
|
72
|
"""
|
73
|
self.newMessage.emit(message)
|
74
|
|
75
|
def onCloseTopic(self, topic: str):
|
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
|
"""
|
89
|
worker: Worker = None
|
90
|
workerThread: QThread = None
|
91
|
|
92
|
def __init__(self):
|
93
|
"""
|
94
|
Constructor - displays all parts of the application.
|
95
|
"""
|
96
|
super(MainView, self).__init__()
|
97
|
|
98
|
self.chartsNum = 0
|
99
|
self.dataDict = {}
|
100
|
self.canvasDict = {}
|
101
|
self.figureDict = {}
|
102
|
self.widgetDict = {}
|
103
|
self.widgetList = []
|
104
|
|
105
|
self.setMinimumSize(QSize(1200, 800))
|
106
|
self.setWindowTitle("MQTT client")
|
107
|
|
108
|
logger = self._createLoggerView()
|
109
|
layout = QVBoxLayout()
|
110
|
layout.addWidget(logger.widget)
|
111
|
|
112
|
widget = QWidget()
|
113
|
widget.setLayout(layout)
|
114
|
self.setCentralWidget(widget)
|
115
|
self._createMenuBar()
|
116
|
|
117
|
scrollArea = QScrollArea(self)
|
118
|
scrollArea.setWidgetResizable(True)
|
119
|
scrollContent = QWidget()
|
120
|
self.grid = QGridLayout(scrollContent)
|
121
|
scrollArea.setWidget(scrollContent)
|
122
|
layout.addWidget(scrollArea)
|
123
|
|
124
|
self.init_subscriber()
|
125
|
|
126
|
def _createLoggerView(self):
|
127
|
"""
|
128
|
Create logger view
|
129
|
"""
|
130
|
logger = LoggerView(self)
|
131
|
formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M')
|
132
|
logger.setFormatter(formatter)
|
133
|
logger.setLevel(logging.INFO)
|
134
|
logging.getLogger('').addHandler(logger)
|
135
|
return logger
|
136
|
|
137
|
def _createMenuBar(self):
|
138
|
"""
|
139
|
Creates menu bar
|
140
|
"""
|
141
|
menuBar = QMenuBar(self)
|
142
|
settingsAction = QAction("&Settings", self)
|
143
|
settingsAction.triggered.connect(self.settings)
|
144
|
menuBar.addAction(settingsAction)
|
145
|
self.setMenuBar(menuBar)
|
146
|
|
147
|
def plot(self, message: Message):
|
148
|
"""
|
149
|
Plots new charts or updates old ones
|
150
|
:param message: message
|
151
|
"""
|
152
|
if message.topic in self.dataDict:
|
153
|
# topic already exists
|
154
|
self.dataDict[message.topic].append(message.value)
|
155
|
|
156
|
figure = self.figureDict[message.topic]
|
157
|
figure.clear()
|
158
|
|
159
|
figure = plt.figure(figure.number)
|
160
|
figure.suptitle(message.topic)
|
161
|
plt.plot(self.dataDict[message.topic])
|
162
|
|
163
|
self.canvasDict[message.topic].draw()
|
164
|
else:
|
165
|
# new topic
|
166
|
self.dataDict[message.topic] = [message.value]
|
167
|
|
168
|
figure = plt.figure(figsize=[500, 500])
|
169
|
canvas = FigureCanvas(figure)
|
170
|
self.layout = QHBoxLayout()
|
171
|
|
172
|
plt.plot(self.dataDict[message.topic])
|
173
|
figure.suptitle(message.topic)
|
174
|
|
175
|
self.canvasDict[message.topic] = canvas
|
176
|
self.figureDict[message.topic] = figure
|
177
|
|
178
|
widget = QWidget()
|
179
|
self.widgetDict[message.topic] = widget
|
180
|
self.widgetList.append(widget)
|
181
|
widget.setLayout(self.layout)
|
182
|
button = QPushButton(':')
|
183
|
button.setFixedSize(QSize(40, 40))
|
184
|
self.layout.addWidget(canvas)
|
185
|
self.layout.addWidget(button)
|
186
|
self.layout.setAlignment(button, QtCore.Qt.AlignTop)
|
187
|
widget.setMinimumSize(QSize(500, 500))
|
188
|
|
189
|
self.grid.addWidget(widget, int(self.chartsNum / 2), self.chartsNum % 2)
|
190
|
|
191
|
self.chartsNum += 1
|
192
|
|
193
|
def deletePlot(self, topic: str):
|
194
|
"""
|
195
|
Deletes plot
|
196
|
:param topic: topic
|
197
|
"""
|
198
|
widget = self.widgetDict[topic]
|
199
|
self.widgetList.remove(widget)
|
200
|
widget.setParent(None)
|
201
|
|
202
|
del self.widgetDict[topic]
|
203
|
del self.canvasDict[topic]
|
204
|
del self.figureDict[topic]
|
205
|
del self.dataDict[topic]
|
206
|
|
207
|
self.reorganizePlots()
|
208
|
|
209
|
def reorganizePlots(self):
|
210
|
"""
|
211
|
Reorganize plots
|
212
|
"""
|
213
|
count = 0
|
214
|
for widget in self.widgetList:
|
215
|
self.grid.addWidget(widget, int(count / 2), count % 2)
|
216
|
count += 1
|
217
|
|
218
|
self.chartsNum -= 1
|
219
|
|
220
|
def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
|
221
|
self.worker.stop()
|
222
|
|
223
|
def settings(self):
|
224
|
"""
|
225
|
Opens settings dialog
|
226
|
"""
|
227
|
dialog = SettingsDialog()
|
228
|
if dialog.exec_():
|
229
|
self.reconnect()
|
230
|
|
231
|
def disconnect(self):
|
232
|
"""
|
233
|
Disconnect
|
234
|
"""
|
235
|
self.worker.stop()
|
236
|
self.workerThread.quit()
|
237
|
self.workerThread.wait()
|
238
|
|
239
|
def reconnect(self):
|
240
|
"""
|
241
|
Reconnect
|
242
|
"""
|
243
|
self.disconnect()
|
244
|
self.worker.params = self.getConfigParams()
|
245
|
self.workerThread.start()
|
246
|
|
247
|
def init_subscriber(self):
|
248
|
"""
|
249
|
Initialization of subscriber
|
250
|
"""
|
251
|
self.workerThread = QThread()
|
252
|
self.worker = Worker(self.getConfigParams())
|
253
|
self.worker.moveToThread(self.workerThread)
|
254
|
self.workerThread.started.connect(self.worker.start)
|
255
|
self.worker.newMessage.connect(
|
256
|
lambda message: self.plot(message)
|
257
|
)
|
258
|
self.worker.closeTopic.connect(
|
259
|
lambda topic: self.deletePlot(topic)
|
260
|
)
|
261
|
self.worker.window = self
|
262
|
self.workerThread.start()
|
263
|
|
264
|
def getConfigParams(self) -> SubscriberParams:
|
265
|
"""
|
266
|
Returns config parameters
|
267
|
:return: config parameters
|
268
|
"""
|
269
|
settings = get_settings()
|
270
|
|
271
|
connection = ConnectionParams(
|
272
|
settings.value("connection_host", DEFAULT_HOST, str),
|
273
|
settings.value("connection_port", DEFAULT_PORT, int),
|
274
|
settings.value("connection_keepalive", DEFAULT_KEEPALIVE, int)
|
275
|
)
|
276
|
|
277
|
params = SubscriberParams(
|
278
|
settings.value("topics_items", DEFAULT_TOPICS),
|
279
|
settings.value("topics_timeout", DEFAULT_TIMEOUT, int),
|
280
|
connection,
|
281
|
settings.value("connection_anonymous", DEFAULT_ANONYMOUS, bool),
|
282
|
settings.value("connection_username", DEFAULT_USERNAME, str),
|
283
|
settings.value("connection_password", DEFAULT_USERNAME, str),
|
284
|
)
|
285
|
|
286
|
return params
|