MQTT Plotter (PyQt5)¶
This utility GUI application is a tool for visualizing the content of multiple data streams passed as short numeric messages back and forth across the network via a MQTT server. It supports opening an authenticated connection to the server, subscribing to a class of messages in order to receive them, viewing message traffic, and plotting the received messages as points in a dynamically updated graphic.
The expected use is that each message will be generated by an Arduino sketch and forwarded to the MQTT network using the Arduino-MQTT Bridge (PyQt5) application. The following section documents the message formatting options.
Data Format¶
Each message received is processed as plain text integer numbers separated by spaces. Message lengths of either two or five value: with two values, they are interpreted as the X and Y location (“X Y”), with five values as a position and an RGB color (“X Y R G B”). All values may range from 0 to 100 inclusive. The topic name is used to identify the point, so multiple messages on the same topic will dynamically move a plot point. Some sample messages follow.
message text |
graphical result on a plotted point |
---|---|
0 0 |
move the point to the lower left corner |
50 50 |
move the point to the center |
100 50 100 0 0 |
move to right edge and paint it red |
75 75 0 0 100 |
move near upper right and paint it blue |
Installation Requirements¶
The code requires a working installation of Python 3 with PyQt5 and paho-mqtt. For suggestions on setting up your system please see Python 3 Installation.
User Guide¶
The connection GUI is derived from MQTT Monitor (PyQt5). The procedure for use generally follows this sequence:
Launch the
qt_mqtt_plotter.py
program using Python 3.Select the port corresponding to your course number.
Enter the username and password provided by your instructor.
Verify that the server address is
mqtt.ideate.cmu.edu
, then click Connect.Enter a subscription pattern according to your needs. This tool is designed to display all messages, so the default wildcard
#
is typical for capturing all messages. However, a pattern such asusername/#
will limit displayed traffic to a particular sender.Verify that data messages are appearing in the text area.
The plot tab will then show an animated graphic in which each topic stream is represented by a single moving point with 2D location and 3D color.
Source Code Documentation¶
If you’re curious about the inner workings of the app, the rest of this page provides detailed documentation. This section is not necessary for using the system.
MainApp¶
MainGUI¶
QtParticles¶
Full Code¶
1#!/usr/bin/env python3
2"""A PyQt5 GUI utility to monitor and plot MQTT server messages."""
3
4################################################################
5# Written in 2018-2020 by Garth Zeglin <garthz@cmu.edu>
6
7# To the extent possible under law, the author has dedicated all copyright
8# and related and neighboring rights to this software to the public domain
9# worldwide. This software is distributed without any warranty.
10
11# You should have received a copy of the CC0 Public Domain Dedication along with this software.
12# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
13
14################################################################
15# standard Python libraries
16from __future__ import print_function
17import os, sys, struct, time, logging, functools, queue, signal, getpass, math
18
19# documentation: https://doc.qt.io/qt-5/index.html
20# documentation: https://www.riverbankcomputing.com/static/Docs/PyQt5/index.html
21from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork
22
23# documentation: https://www.eclipse.org/paho/clients/python/docs/
24import paho.mqtt.client as mqtt
25
26# default logging output
27log = logging.getLogger('main')
28
29# logger to pass to the MQTT library
30mqtt_log = logging.getLogger('mqtt')
31mqtt_log.setLevel(logging.WARNING)
32
33# IDeATE server instances, as per https://mqtt.ideate.cmu.edu/#ports
34
35ideate_ports = { 8884 : '16-223',
36 8885 : '16-375',
37 8886 : '60-223',
38 8887 : '62-362',
39}
40
41mqtt_rc_codes = ['Success', 'Incorrect protocol version', 'Invalid client identifier', 'Server unavailable', 'Bad username or password', 'Not authorized']
42
43################################################################
44class QtParticles(QtWidgets.QWidget):
45 """Custom widget to draw a 2D plot of a set of particles. Each particle has 5-D
46 state: [x, y, r, g, b]. Each axis is defined to have unit scaling and is
47 valid on [0,1].
48 """
49
50 def __init__(self):
51 super().__init__()
52 self.setMinimumSize(QtCore.QSize(100, 100))
53 self.setAutoFillBackground(True)
54
55 # Graphical state variables. Define a placeholder point while testing.
56 # self.particles = {'placeholder' : [0.5, 0.5, 0.5, 0.5, 0.5] }
57 self.particles = {}
58
59 # finish initialization
60 self.show()
61 return
62
63 # === particle update methods ============================================================
64 def update_particle_position(self, name, location):
65 # limit the coordinate range to slightly larger than the plotting bounds
66 x = min(max(location[0], -0.1), 1.1)
67 y = min(max(location[1], -0.1), 1.1)
68
69 particle = self.particles.get(name)
70 if particle is None:
71 self.particles[name] = [x, y, 0.5, 0.5, 0.5]
72 else:
73 particle[0:2] = x, y
74 self.repaint()
75
76 def update_particle_color(self, name, rgb):
77 particle = self.particles.get(name)
78 if particle is None:
79 self.particles[name] = [0.0, 0.0, rgb[0], rgb[1], rgb[2]]
80 else:
81 particle[2:5] = rgb
82 self.repaint()
83
84 # === Qt API methods ============================================================
85 def paintEvent(self, e):
86 """Subclass implementation of parent QWidget class callback to repaint the graphics."""
87 geometry = self.geometry()
88 view_width = geometry.width()
89 view_height = geometry.height()
90
91 # Clear the background.
92 qp = QtGui.QPainter()
93 qp.begin(self)
94 qp.fillRect(QtCore.QRectF(0, 0, view_width, view_height), QtCore.Qt.white)
95 # qp.setRenderHint(QtGui.QPainter.Antialiasing)
96
97 # Set up a coordinate system scaled to unit dimension that keeps the
98 # minimum visible area in view.
99 scene_width = 1.3 # minimum visible width
100 scene_height = scene_width # minimum visible height
101 scene_aspect = scene_width / scene_height
102 view_aspect = view_width / view_height
103 if scene_aspect > view_aspect:
104 scaling = view_width / scene_width
105 else:
106 scaling = view_height/scene_height
107
108 # Capture the default graphics transformation.
109 qp.save()
110
111 # Move the origin to the center (in pixel coordinates).
112 qp.translate(QtCore.QPointF(view_width/2, view_height/2))
113
114 # Apply scaling to draw in unit coordinates.
115 qp.scale(scaling, scaling)
116
117 # Translate in the new scaled coordinates to place the origin near the
118 # upper left corner; the default coordinates using +Y pointing down.
119 qp.translate(QtCore.QPointF(-0.5, -0.5))
120
121 # Draw the bounds of the unit square.
122 pen = QtGui.QPen(QtCore.Qt.black)
123 pen.setWidthF(0.005)
124 qp.setPen(pen)
125 qp.drawRect(QtCore.QRectF(0.0, 0.0, 1.0, 1.0))
126
127 # Draw the particles. The Y dimension is inverted to a normal
128 # mathematical plot with +Y up.
129 color = QtGui.QColor()
130 for particle in self.particles.values():
131 color.setRgbF(min(1.0, abs(particle[2])), min(1.0, abs(particle[3])), min(1.0, abs(particle[4])), 1.0)
132 brush = QtGui.QBrush(color)
133 qp.setBrush(brush)
134 qp.drawEllipse(QtCore.QPointF(particle[0], 1.0 - particle[1]), 0.02, 0.02)
135
136 # Restore the initial unscaled coordinates.
137 qp.restore()
138 qp.end()
139
140
141################################################################
142class MainGUI(QtWidgets.QMainWindow):
143 """A custom main window which provides all GUI controls. Requires a delegate main application object to handle user requests."""
144
145 def __init__(self, main, *args, **kwargs):
146 super(MainGUI,self).__init__()
147
148 # save the main object for delegating GUI events
149 self.main = main
150
151 # create the GUI elements
152 self.console_queue = queue.Queue()
153 self.setupUi()
154
155 self._handler = None
156 self.enable_console_logging()
157
158 # finish initialization
159 self.show()
160
161 # manage the console output across threads
162 self.console_timer = QtCore.QTimer()
163 self.console_timer.timeout.connect(self._poll_console_queue)
164 self.console_timer.start(50) # units are milliseconds
165
166 return
167
168 # ------------------------------------------------------------------------------------------------
169 def setupUi(self):
170 self.setWindowTitle("IDeATe MQTT Plotter")
171 self.resize(600, 600)
172
173 # set up tabbed page structure
174 self.tabs = QtWidgets.QTabWidget()
175 self.setCentralWidget(self.tabs)
176
177 # set up a main tab with the connection controls
178 self.mainTab = QtWidgets.QWidget(self)
179 self.tabs.addTab(self.mainTab, 'Main')
180 self.verticalLayout = QtWidgets.QVBoxLayout(self.mainTab)
181 self.verticalLayout.setContentsMargins(-1, -1, -1, 9) # left, top, right, bottom
182
183 # generate GUI for configuring the MQTT connection
184
185 # server name entry and port selection
186 hbox = QtWidgets.QHBoxLayout()
187 self.verticalLayout.addLayout(hbox)
188 hbox.addWidget(QtWidgets.QLabel("MQTT server address:"))
189 self.mqtt_server_name = QtWidgets.QLineEdit()
190 self.mqtt_server_name.setText(str(self.main.hostname))
191 self.mqtt_server_name.editingFinished.connect(self.mqtt_server_name_entered)
192 hbox.addWidget(self.mqtt_server_name)
193
194 hbox.addWidget(QtWidgets.QLabel("port:"))
195 self.port_selector = QtWidgets.QComboBox()
196 hbox.addWidget(self.port_selector)
197
198 self.port_selector.addItem("")
199 for pairs in ideate_ports.items():
200 self.port_selector.addItem("%d (%s)" % pairs)
201 self.port_selector.activated['QString'].connect(self.mqtt_port_selected)
202
203 # attempt to pre-select the stored port number
204 try:
205 idx = list(ideate_ports.keys()).index(self.main.portnum)
206 self.port_selector.setCurrentIndex(idx+1)
207 except ValueError:
208 pass
209
210 # instructions
211 explanation = QtWidgets.QLabel("""Username and password provided by instructor. Please see help tab for details.""")
212 explanation.setWordWrap(True)
213 self.verticalLayout.addWidget(explanation)
214
215 # user and password entry
216 hbox = QtWidgets.QHBoxLayout()
217 self.verticalLayout.addLayout(hbox)
218 hbox.addWidget(QtWidgets.QLabel("MQTT username:"))
219 self.mqtt_username = QtWidgets.QLineEdit()
220 self.mqtt_username.setText(str(self.main.username))
221 self.mqtt_username.editingFinished.connect(self.mqtt_username_entered)
222 hbox.addWidget(self.mqtt_username)
223
224 hbox.addWidget(QtWidgets.QLabel("password:"))
225 self.mqtt_password = QtWidgets.QLineEdit()
226 self.mqtt_password.setText(str(self.main.password))
227 self.mqtt_password.editingFinished.connect(self.mqtt_password_entered)
228 hbox.addWidget(self.mqtt_password)
229
230 # instructions
231 explanation = QtWidgets.QLabel("""A subscription specifies topics to receive. Please see help tab for details.""")
232 explanation.setWordWrap(True)
233 self.verticalLayout.addWidget(explanation)
234
235 # subscription topic entry
236 hbox = QtWidgets.QHBoxLayout()
237 label = QtWidgets.QLabel("MQTT message subscription:")
238 self.mqtt_sub = QtWidgets.QLineEdit()
239 self.mqtt_sub.setText(self.main.subscription)
240 self.mqtt_sub.editingFinished.connect(self.mqtt_sub_entered)
241 hbox.addWidget(label)
242 hbox.addWidget(self.mqtt_sub)
243 self.verticalLayout.addLayout(hbox)
244
245 # connection indicator
246 self.connected = QtWidgets.QLabel()
247 self.connected.setLineWidth(3)
248 self.connected.setFrameStyle(QtWidgets.QFrame.Box)
249 self.connected.setAlignment(QtCore.Qt.AlignCenter)
250 sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
251 self.connected.setSizePolicy(sizePolicy)
252 self.set_connected_state(False)
253
254 # connection control buttons
255 connect = QtWidgets.QPushButton('Connect')
256 connect.pressed.connect(self.connection_requested)
257 disconnect = QtWidgets.QPushButton('Disconnect')
258 disconnect.pressed.connect(self.main.disconnect_from_mqtt_server)
259 hbox = QtWidgets.QHBoxLayout()
260 hbox.addWidget(self.connected)
261 hbox.addWidget(connect)
262 hbox.addWidget(disconnect)
263 self.verticalLayout.addLayout(hbox)
264
265 # text area for displaying both internal and received messages
266 self.consoleOutput = QtWidgets.QPlainTextEdit()
267 self.consoleOutput.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
268 self.verticalLayout.addWidget(self.consoleOutput)
269
270 # set up the graphics tab
271 self.plot = QtParticles()
272 self.tabs.addTab(self.plot, 'Plot')
273
274 # set up the help tab
275 self.helpTab = QtWidgets.QWidget(self)
276 self.tabs.addTab(self.helpTab, 'Help')
277 self._make_help(self.helpTab)
278
279 # set up the status bar which appears at the bottom of the window
280 self.statusbar = QtWidgets.QStatusBar(self)
281 self.setStatusBar(self.statusbar)
282
283 # set up the main menu
284 self.menubar = QtWidgets.QMenuBar(self)
285 self.menubar.setGeometry(QtCore.QRect(0, 0, 500, 22))
286 self.menubar.setNativeMenuBar(False)
287 self.menubar.setObjectName("menubar")
288 self.menuTitle = QtWidgets.QMenu(self.menubar)
289 self.setMenuBar(self.menubar)
290 self.actionQuit = QtWidgets.QAction(self)
291 self.menuTitle.addAction(self.actionQuit)
292 self.menubar.addAction(self.menuTitle.menuAction())
293 self.menuTitle.setTitle("File")
294 self.actionQuit.setText("Quit")
295 self.actionQuit.setShortcut("Ctrl+Q")
296 self.actionQuit.triggered.connect(self.quitSelected)
297
298 return
299
300 # --- verbose function to create the help tab -----------------------------------------
301 def _make_help(self, parent):
302 vbox = QtWidgets.QVBoxLayout(parent)
303 hbox = QtWidgets.QHBoxLayout()
304 vbox.addLayout(hbox)
305 text = QtWidgets.QTextEdit()
306 hbox.addWidget(text)
307 text.insertHtml("""
308<style type="text/css">
309table { margin-left: 20px; }
310td { padding-left: 20px; }
311</style>
312<a href="#top"></a>
313<h1>IDeATe MQTT Plotter</h1>
314<p>This Python application is a tool intended for visualizing the content of multiple data streams passed as short numeric messages back and forth across the network via a MQTT server. It supports opening an authenticated connection to the server, subscribing to a class of messages in order to receive them, viewing message traffic, and plotting the received messages as points in a dynamically updated graphic.</p>
315<h2>Connecting</h2>
316<p>The first set of controls configures server parameters before attempting a connection. Changes will not take effect until the next connection attempt.</p
317
318<dl>
319 <dt>server address</dt><dd>The network name of the MQTT server. (Defaults to mqtt.ideate.cmu.edu.)</dd>
320 <dt>server port</dt><dd>The numeric port number for the MQTT server. IDeATe is using a separate server for each course, so the drop-down menu also identifies the associated course number.</dd>
321 <dt>username</dt><dd>Server-specific identity, chosen by your instructor.</dd>
322 <dt>password</dt><dd>Server-specific password, chosen by your instructor.</dd>
323</dl>
324
325<p>Your username and password is specific to the MQTT server and will be provided by your instructor. This may be individual or may be a shared login for all students in the course. Please note, the password will not be your Andrew password.</p>
326
327<h2>Listening</h2>
328
329<p>MQTT works on a publish/subscribe model in which messages are published on <i>topics</i> identified by a topic name. The name is structured like a path string separated by <tt>/</tt> characters to organize messages into a hierarchy of topics and subtopics.
330Our course policy will be to prefix all topics with a student andrew ID, e.g. if your user name is xyzzy, we ask that you publish on the 'xyzzy' topic and sub-topics, as per the following examples.</p>
331
332
333<p>
334<table>
335<tr><td><b>xyzzy</b></td><td>top-level topic on which user 'xyzzy' should publish</td></tr>
336<tr><td><b>xyzzy/status</b></td><td>a sub-topic on which user 'xyzzy' could publish</td></tr>
337<tr><td><b>xyzzy/sensor</b></td><td>another sub-topic on which user 'xyzzy' could publish</td></tr>
338<tr><td><b>xyzzy/sensor/1</b></td><td>a possible sub-sub-topic</td></tr>
339</table>
340</p>
341
342<p>The message subscription field specifies topics to receive. The subscription may include a # character as a wildcard, as per the following examples.</p>
343<p><table>
344<tr><td><b>#</b></td><td>subscribe to all messages (typical for this application)</td></tr>
345<tr><td><b>xyzzy</b></td><td>subscribe to the top-level published messages for user xyzzy</td></tr>
346<tr><td><b>xyzzy/#</b></td><td>subscribe to all published messages for user xyzzy, including subtopics</td></tr>
347</table>
348</p>
349<p>Changing the subscription field immediately changes what is received; the monitor unsubscribes from the previous pattern and subscribes to the new one. Entering an empty field defaults to the global pattern '#'.</p>
350
351<p>The large text field is the console area which shows both debugging and status log messages as well as received messages.</p>
352
353<h2>Data Format</h2>
354
355<p>Each message received is processed as plain text integer numbers separated by spaces. Either two or five value messages are supported: with two values, they are interpreted as the X and Y location ("X Y"), with five values as a position and an RGB color ("X Y R G B"). All values may range from 0 to 100 inclusive. The topic name is used to identify the point, so multiple messages on the same topic will dynamically move a plot point. Some sample messages follow.</p>
356
357<p><table>
358<tr><td><b>0 0</b></td><td>move the point to the lower left corner</td></tr>
359<tr><td><b>50 50</b></td><td>move the point to the center</td></tr>
360<tr><td><b>100 50 100 0 0</b></td><td>move to right edge and paint it red</td></tr>
361<tr><td><b>75 75 0 0 100</b></td><td>move near upper right and paint it blue</td></tr>
362</table>
363</p>
364
365<h2>More Information</h2>
366
367<p>The IDeATE server has more detailed information on the server help page at <b>https://mqtt.ideate.cmu.edu</b></p>
368
369""")
370 text.scrollToAnchor("top")
371 text.setReadOnly(True)
372 return
373
374 # --- logging to screen -------------------------------------------------------------
375 def enable_console_logging(self):
376 # get the root logger to receive all logging traffic
377 logger = logging.getLogger()
378 # create a logging handler which writes to the console window via self.write
379 handler = logging.StreamHandler(self)
380 handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
381 logger.addHandler(handler)
382 # logger.setLevel(logging.NOTSET)
383 logger.setLevel(logging.DEBUG)
384 # logger.setLevel(logging.WARNING)
385 handler.setLevel(logging.NOTSET)
386 self._handler = handler
387 log.info("Enabled logging in console window.")
388 return
389
390 def disable_console_logging(self):
391 if self._handler is not None:
392 logging.getLogger().removeHandler(self._handler)
393 self._handler = None
394
395 # --- window and qt event processing -------------------------------------------------------------
396 def show_status(self, string):
397 self.statusbar.showMessage(string)
398
399 def _poll_console_queue(self):
400 """Write any queued console text to the console text area from the main thread."""
401 while not self.console_queue.empty():
402 string = str(self.console_queue.get())
403 stripped = string.rstrip()
404 if stripped != "":
405 self.consoleOutput.appendPlainText(stripped)
406 return
407
408 def write(self, string):
409 """Write output to the console text area in a thread-safe way. Qt only allows
410 calls from the main thread, but the service routines run on separate threads."""
411 self.console_queue.put(string)
412 return
413
414 def quitSelected(self):
415 self.write("User selected quit.")
416 self.close()
417
418 def closeEvent(self, event):
419 self.write("Received window close event.")
420 self.main.app_is_exiting()
421 self.disable_console_logging()
422 super(MainGUI,self).closeEvent(event)
423
424 def set_connected_state(self, flag):
425 if flag is True:
426 self.connected.setText(" Connected ")
427 self.connected.setStyleSheet("color: white; background-color: green;")
428 else:
429 self.connected.setText(" Not Connected ")
430 self.connected.setStyleSheet("color: white; background-color: blue;")
431
432
433 # --- GUI widget event processing ----------------------------------------------------------------------
434
435 def mqtt_server_name_entered(self):
436 name = self.mqtt_server_name.text()
437 self.write("Server name changed: %s" % name)
438 self.main.set_server_name(name)
439
440 def decode_port_selection(self):
441 title = self.port_selector.currentText()
442 if title == "":
443 return None
444 else:
445 return int(title.split()[0]) # convert the first token to a number
446
447 def mqtt_port_selected(self, title):
448 portnum = self.decode_port_selection()
449 self.write("Port selection changed: %s" % title)
450 self.main.set_server_port(portnum)
451
452 def mqtt_username_entered(self):
453 name = self.mqtt_username.text()
454 self.write("User name changed: %s" % name)
455 self.main.set_username(name)
456
457 def mqtt_password_entered(self):
458 name = self.mqtt_password.text()
459 self.write("Password changed: %s" % name)
460 self.main.set_password(name)
461
462 def connection_requested(self):
463 # When the connect button is pressed, make sure all fields are up to
464 # date. It is otherwise possible to leave a text field selected with
465 # unreceived changes while pressing Connect.
466 hostname = self.mqtt_server_name.text()
467 portnum = self.decode_port_selection()
468 username = self.mqtt_username.text()
469 password = self.mqtt_password.text()
470
471 self.main.set_server_name(hostname)
472 self.main.set_server_port(portnum)
473 self.main.set_username(username)
474 self.main.set_password(password)
475
476 self.main.connect_to_mqtt_server()
477
478 def mqtt_sub_entered(self):
479 sub = self.mqtt_sub.text()
480 if sub == '':
481 self.mqtt_sub.setText("#")
482 sub = "#"
483
484 self.write("Subscription changed to: %s" % sub)
485 self.main.set_subscription(sub)
486
487################################################################
488class MainApp(QtCore.QObject):
489 """Main application object holding any non-GUI related state."""
490
491 # class variable with Qt signal used to communicate between network thread and main thread
492 _messageReceived = QtCore.pyqtSignal(str, bytes, name='_messageReceived')
493
494 def __init__(self):
495
496 super(MainApp,self).__init__()
497 # Attach a handler to the keyboard interrupt (control-C).
498 signal.signal(signal.SIGINT, self._sigint_handler)
499
500 # load any available persistent application settings
501 QtCore.QCoreApplication.setOrganizationName("IDeATe")
502 QtCore.QCoreApplication.setOrganizationDomain("ideate.cmu.edu")
503 QtCore.QCoreApplication.setApplicationName('mqtt_plotter')
504 self.settings = QtCore.QSettings()
505
506 # uncomment to restore 'factory defaults'
507 # self.settings.clear()
508
509 # MQTT server settings
510 self.hostname = self.settings.value('mqtt_host', 'mqtt.ideate.cmu.edu')
511 self.portnum = self.settings.value('mqtt_port', None)
512 self.username = self.settings.value('mqtt_user', 'students')
513 self.password = self.settings.value('mqtt_password', '(not yet entered)')
514
515 # Create a default subscription based on the username. The hash mark is a wildcard.
516 username = getpass.getuser()
517
518 # self.subscription = self.settings.value('mqtt_subscription', username + '/#')
519 self.subscription = self.settings.value('mqtt_subscription', '#')
520
521 # create the interface window
522 self.window = MainGUI(self)
523
524 # Initialize the MQTT client system
525 self.client = mqtt.Client()
526 self.client.enable_logger(mqtt_log)
527 self.client.on_log = self.on_log
528 self.client.on_connect = self.on_connect
529 self.client.on_disconnect = self.on_disconnect
530 self.client.on_message = self.on_message
531 self.client.tls_set()
532
533 # Connect the signal used to transfer received messages from the network server thread to the main thread.
534 self._messageReceived.connect(self.process_message)
535
536 self.window.show_status("Please set the MQTT server address and select Connect.")
537 return
538
539 ################################################################
540 def app_is_exiting(self):
541 if self.client.is_connected():
542 self.client.disconnect()
543 self.client.loop_stop()
544
545 def _sigint_handler(self, signal, frame):
546 print("Keyboard interrupt caught, running close handlers...")
547 self.app_is_exiting()
548 sys.exit(0)
549
550 ################################################################
551 def set_server_name(self, name):
552 self.hostname = name
553 self.settings.setValue('mqtt_host', name)
554
555 def set_server_port(self, value):
556 self.portnum = value
557 self.settings.setValue('mqtt_port', self.portnum)
558
559 def set_username(self, name):
560 self.username = name
561 self.settings.setValue('mqtt_user', name)
562
563 def set_password(self, name):
564 self.password = name
565 self.settings.setValue('mqtt_password', name)
566
567 def connect_to_mqtt_server(self):
568 if self.client.is_connected():
569 self.window.write("Already connected.")
570 else:
571 if self.portnum is None:
572 log.warning("Please specify the server port before attempting connection.")
573 else:
574 log.debug("Initiating MQTT connection to %s:%d" % (self.hostname, self.portnum))
575 self.window.write("Attempting connection.")
576 self.client.username_pw_set(self.username, self.password)
577 self.client.connect_async(self.hostname, self.portnum)
578 self.client.loop_start()
579
580 def disconnect_from_mqtt_server(self):
581 if self.client.is_connected():
582 self.client.disconnect()
583 else:
584 self.window.write("Not connected.")
585 self.client.loop_stop()
586
587 ################################################################
588 # The callback for when the broker responds to our connection request.
589 def on_connect(self, client, userdata, flags, rc):
590 self.window.write("Connected to server with with flags: %s, result code: %s" % (flags, rc))
591
592 if rc == 0:
593 log.info("Connection succeeded.")
594
595 elif rc > 0:
596 if rc < len(mqtt_rc_codes):
597 log.warning("Connection failed with error: %s", mqtt_rc_codes[rc])
598 else:
599 log.warning("Connection failed with unknown error %d", rc)
600
601 # Subscribing in on_connect() means that if we lose the connection and reconnect then subscriptions will be renewed.
602 client.subscribe(self.subscription)
603 self.window.show_status("Connected.")
604 self.window.set_connected_state(True)
605 return
606
607 # The callback for when the broker responds with error messages.
608 def on_log(client, userdata, level, buf):
609 log.debug("on_log level %s: %s", level, userdata)
610 return
611
612 def on_disconnect(self, client, userdata, rc):
613 log.debug("disconnected")
614 self.window.write("Disconnected from server.")
615 self.window.show_status("Disconnected.")
616 self.window.set_connected_state(False)
617
618 # The callback for when a message has been received on a topic to which this
619 # client is subscribed. The message variable is a MQTTMessage that describes
620 # all of the message parameters.
621
622 # Some useful MQTTMessage fields: topic, payload, qos, retain, mid, properties.
623 # The payload is a binary string (bytes).
624 # qos is an integer quality of service indicator (0,1, or 2)
625 # mid is an integer message ID.
626
627 # N.B. this function is called from the network server thread, but the actual
628 # processing needs to happen on the main thread for graphic output.
629 def on_message(self, client, userdata, msg):
630 self.window.write("{%s} %s" % (msg.topic, msg.payload))
631 self._messageReceived.emit(msg.topic, msg.payload)
632
633 @QtCore.pyqtSlot(str, bytes)
634 def process_message(self, topic, payload):
635 # Update the particle plotter. The name of the particle is the topic
636 # name, and the data updated depends on the message format.
637 name = topic
638
639 # Parse the message text by attempting to convert all tokens to integers. The values
640 # are scaled so that the default range is 0 to 100.
641 tokens = payload.split()
642 try:
643 values = [(0.01 * int(x)) for x in tokens]
644 if len(values) > 1:
645 self.window.plot.update_particle_position(name, values[0:2])
646 if len(values) > 4:
647 self.window.plot.update_particle_color(name, values[2:5])
648
649 except ValueError:
650 log.warning("Error parsing message into numbers: %s %s", topic, payload)
651
652 return
653
654 ################################################################
655 def set_subscription(self, sub):
656 if self.client.is_connected():
657 self.client.unsubscribe(self.subscription)
658 try:
659 self.client.subscribe(sub)
660 self.subscription = sub
661 self.settings.setValue('mqtt_subscription', sub)
662 except ValueError:
663 self.window.write("Invalid subscription string, not changed.")
664 self.client.subscribe(self.subscription)
665 else:
666 self.subscription = sub
667 self.settings.setValue('mqtt_subscription', sub)
668
669 ################################################################
670
671def main():
672 # Optionally add an additional root log handler to stream messages to the terminal console.
673 if False:
674 console_handler = logging.StreamHandler()
675 console_handler.setLevel(logging.DEBUG)
676 console_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
677 logging.getLogger().addHandler(console_handler)
678
679 # initialize the Qt system itself
680 app = QtWidgets.QApplication(sys.argv)
681
682 # create the main application controller
683 main = MainApp()
684
685 # run the event loop until the user is done
686 log.info("Starting event loop.")
687 sys.exit(app.exec_())
688
689################################################################
690# Main script follows. This sequence is executed when the script is initiated from the command line.
691
692if __name__ == "__main__":
693 main()