MQTT Monitor (PyQt5)

This utility GUI application is a tool for debugging programs using a remote MQTT server. It can show messages on multiple topics and publish text-based messages on a single topic.

../_images/qt_mqtt_monitor.png

The MQTT monitor application in operation. The large button at the top brings up detailed help. This utility may be helpful for debugging other MQTT applications.

The application is provided all in one file and can be be directly downloaded from qt_mqtt_monitor.py. The following sections have both documentation and the full code.

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 procedure for use generally follows this sequence:

  1. Launch the qt_mqtt_monitor.py program using Python 3.

  2. Select the port corresponding to your course number.

  3. Enter the username and password provided by your instructor.

  4. Verify that the server address is mqtt.ideate.cmu.edu, then click Connect.

  5. Enter a subscription pattern according to your needs. The default # will show all traffic. A pattern such as username/# will limit displayed traffic to a particular sender.

  6. Enter a transmission topic according to your needs. By convention we are using Andrew IDs to identify our own transmitted messages.

  7. Enter text to transmit in the data field. Pressing enter will send the current message; pressing enter multiple times will retransmit the same message.

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

class mqtt.qt_mqtt_monitor.MainApp[source]

Main application object holding any non-GUI related state.

MainGUI

class mqtt.qt_mqtt_monitor.MainGUI(main, *args, **kwargs)[source]

A custom main window which provides all GUI controls. Requires a delegate main application object to handle user requests.

closeEvent(self, QCloseEvent)[source]
write(string)[source]

Write output to the console text area in a thread-safe way. Qt only allows calls from the main thread, but the service routines run on separate threads.

Full Code

  1#!/usr/bin/env python3
  2"""A PyQt5 GUI utility to monitor and send 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
 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 MainGUI(QtWidgets.QMainWindow):
 45    """A custom main window which provides all GUI controls.  Requires a delegate main application object to handle user requests."""
 46
 47    def __init__(self, main, *args, **kwargs):
 48        super(MainGUI,self).__init__()
 49
 50        # save the main object for delegating GUI events
 51        self.main = main
 52
 53        # create the GUI elements
 54        self.console_queue = queue.Queue()
 55        self.setupUi()
 56
 57        self._handler = None
 58        self.enable_console_logging()
 59
 60        # finish initialization
 61        self.show()
 62
 63        # manage the console output across threads
 64        self.console_timer = QtCore.QTimer()
 65        self.console_timer.timeout.connect(self._poll_console_queue)
 66        self.console_timer.start(50)  # units are milliseconds
 67
 68        return
 69
 70    # ------------------------------------------------------------------------------------------------
 71    def setupUi(self):
 72        self.setWindowTitle("IDeATe MQTT Monitor")
 73        self.resize(600, 600)
 74
 75        self.centralwidget = QtWidgets.QWidget(self)
 76        self.setCentralWidget(self.centralwidget)
 77        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
 78        self.verticalLayout.setContentsMargins(-1, -1, -1, 9) # left, top, right, bottom
 79
 80        # help panel button
 81        help = QtWidgets.QPushButton('Open the Help Panel')
 82        help.pressed.connect(self.help_requested)
 83        self.verticalLayout.addWidget(help)
 84
 85        # generate GUI for configuring the MQTT connection
 86
 87        # server name entry and port selection
 88        hbox = QtWidgets.QHBoxLayout()
 89        self.verticalLayout.addLayout(hbox)
 90        hbox.addWidget(QtWidgets.QLabel("MQTT server address:"))
 91        self.mqtt_server_name = QtWidgets.QLineEdit()
 92        self.mqtt_server_name.setText(str(self.main.hostname))
 93        self.mqtt_server_name.editingFinished.connect(self.mqtt_server_name_entered)
 94        hbox.addWidget(self.mqtt_server_name)
 95
 96        hbox.addWidget(QtWidgets.QLabel("port:"))
 97        self.port_selector = QtWidgets.QComboBox()
 98        hbox.addWidget(self.port_selector)
 99
100        self.port_selector.addItem("")
101        for pairs in ideate_ports.items():
102            self.port_selector.addItem("%d (%s)" % pairs)
103        self.port_selector.activated['QString'].connect(self.mqtt_port_selected)
104
105        # attempt to pre-select the stored port number
106        try:
107            idx = list(ideate_ports.keys()).index(self.main.portnum)
108            self.port_selector.setCurrentIndex(idx+1)
109        except ValueError:
110            pass
111
112        # instructions
113        explanation = QtWidgets.QLabel("""Username and password provided by instructor.  Please see help panel for details.""")
114        explanation.setWordWrap(True)
115        self.verticalLayout.addWidget(explanation)
116
117        # user and password entry
118        hbox = QtWidgets.QHBoxLayout()
119        self.verticalLayout.addLayout(hbox)
120        hbox.addWidget(QtWidgets.QLabel("MQTT username:"))
121        self.mqtt_username = QtWidgets.QLineEdit()
122        self.mqtt_username.setText(str(self.main.username))
123        self.mqtt_username.editingFinished.connect(self.mqtt_username_entered)
124        hbox.addWidget(self.mqtt_username)
125
126        hbox.addWidget(QtWidgets.QLabel("password:"))
127        self.mqtt_password = QtWidgets.QLineEdit()
128        self.mqtt_password.setText(str(self.main.password))
129        self.mqtt_password.editingFinished.connect(self.mqtt_password_entered)
130        hbox.addWidget(self.mqtt_password)
131
132        # instructions
133        explanation = QtWidgets.QLabel("""A subscription specifies topics to receive.  Please see help panel for details.""")
134        explanation.setWordWrap(True)
135        self.verticalLayout.addWidget(explanation)
136
137        # subscription topic entry
138        hbox = QtWidgets.QHBoxLayout()
139        label = QtWidgets.QLabel("MQTT message subscription:")
140        self.mqtt_sub = QtWidgets.QLineEdit()
141        self.mqtt_sub.setText(self.main.subscription)
142        self.mqtt_sub.editingFinished.connect(self.mqtt_sub_entered)
143        hbox.addWidget(label)
144        hbox.addWidget(self.mqtt_sub)
145        self.verticalLayout.addLayout(hbox)
146
147        # connection indicator
148        self.connected = QtWidgets.QLabel()
149        self.connected.setLineWidth(3)
150        self.connected.setFrameStyle(QtWidgets.QFrame.Box)
151        self.connected.setAlignment(QtCore.Qt.AlignCenter)
152        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
153        self.connected.setSizePolicy(sizePolicy)
154        self.set_connected_state(False)
155
156        # connection control buttons
157        connect = QtWidgets.QPushButton('Connect')
158        connect.pressed.connect(self.connection_requested)
159        disconnect = QtWidgets.QPushButton('Disconnect')
160        disconnect.pressed.connect(self.main.disconnect_from_mqtt_server)
161        hbox = QtWidgets.QHBoxLayout()
162        hbox.addWidget(self.connected)
163        hbox.addWidget(connect)
164        hbox.addWidget(disconnect)
165        self.verticalLayout.addLayout(hbox)
166
167        # text area for displaying both internal and received messages
168        self.consoleOutput = QtWidgets.QPlainTextEdit()
169        self.consoleOutput.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
170        self.verticalLayout.addWidget(self.consoleOutput)
171
172        # instructions
173        explanation = QtWidgets.QLabel("""Pressing enter in the data field will broadcast the string on the given topic.""")
174        explanation.setWordWrap(True)
175        self.verticalLayout.addWidget(explanation)
176
177        # message topic entry
178        hbox = QtWidgets.QHBoxLayout()
179        label = QtWidgets.QLabel("MQTT message topic:")
180        self.mqtt_topic = QtWidgets.QLineEdit()
181        self.mqtt_topic.setText(self.main.topic)
182        self.mqtt_topic.editingFinished.connect(self.mqtt_topic_entered)
183        hbox.addWidget(label)
184        hbox.addWidget(self.mqtt_topic)
185        self.verticalLayout.addLayout(hbox)
186
187        # message payload entry
188        hbox = QtWidgets.QHBoxLayout()
189        label = QtWidgets.QLabel("MQTT message data:")
190        self.mqtt_payload = QtWidgets.QLineEdit()
191        self.mqtt_payload.setText(self.main.payload)
192        self.mqtt_payload.returnPressed.connect(self.mqtt_payload_entered)
193        hbox.addWidget(label)
194        hbox.addWidget(self.mqtt_payload)
195        self.verticalLayout.addLayout(hbox)
196
197        # set up the status bar which appears at the bottom of the window
198        self.statusbar = QtWidgets.QStatusBar(self)
199        self.setStatusBar(self.statusbar)
200
201        # set up the main menu
202        self.menubar = QtWidgets.QMenuBar(self)
203        self.menubar.setGeometry(QtCore.QRect(0, 0, 500, 22))
204        self.menubar.setNativeMenuBar(False)
205        self.menubar.setObjectName("menubar")
206        self.menuTitle = QtWidgets.QMenu(self.menubar)
207        self.setMenuBar(self.menubar)
208        self.actionQuit = QtWidgets.QAction(self)
209        self.menuTitle.addAction(self.actionQuit)
210        self.menubar.addAction(self.menuTitle.menuAction())
211        self.menuTitle.setTitle("File")
212        self.actionQuit.setText("Quit")
213        self.actionQuit.setShortcut("Ctrl+Q")
214        self.actionQuit.triggered.connect(self.quitSelected)
215
216        return
217
218    # --- logging to screen -------------------------------------------------------------
219    def enable_console_logging(self):
220        # get the root logger to receive all logging traffic
221        logger = logging.getLogger()
222        # create a logging handler which writes to the console window via self.write
223        handler = logging.StreamHandler(self)
224        handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
225        logger.addHandler(handler)
226        # logger.setLevel(logging.NOTSET)
227        logger.setLevel(logging.DEBUG)
228        handler.setLevel(logging.NOTSET)
229        self._handler = handler
230        log.info("Enabled logging in console window.")
231        return
232
233    def disable_console_logging(self):
234        if self._handler is not None:
235            logging.getLogger().removeHandler(self._handler)
236            self._handler = None
237
238    # --- window and qt event processing -------------------------------------------------------------
239    def show_status(self, string):
240        self.statusbar.showMessage(string)
241
242    def _poll_console_queue(self):
243        """Write any queued console text to the console text area from the main thread."""
244        while not self.console_queue.empty():
245            string = str(self.console_queue.get())
246            stripped = string.rstrip()
247            if stripped != "":
248                self.consoleOutput.appendPlainText(stripped)
249        return
250
251    def write(self, string):
252        """Write output to the console text area in a thread-safe way.  Qt only allows
253        calls from the main thread, but the service routines run on separate threads."""
254        self.console_queue.put(string)
255        return
256
257    def quitSelected(self):
258        self.write("User selected quit.")
259        self.close()
260
261    def closeEvent(self, event):
262        self.write("Received window close event.")
263        self.main.app_is_exiting()
264        self.disable_console_logging()
265        super(MainGUI,self).closeEvent(event)
266
267    def set_connected_state(self, flag):
268        if flag is True:
269            self.connected.setText("  Connected   ")
270            self.connected.setStyleSheet("color: white; background-color: green;")
271        else:
272            self.connected.setText(" Not Connected ")
273            self.connected.setStyleSheet("color: white; background-color: blue;")
274
275
276    # --- GUI widget event processing ----------------------------------------------------------------------
277
278    def help_requested(self):
279        panel = QtWidgets.QDialog(self)
280        panel.resize(600,400)
281        panel.setWindowTitle("IDeATe MQTT Monitor: Help Info")
282        vbox = QtWidgets.QVBoxLayout(panel)
283        hbox = QtWidgets.QHBoxLayout()
284        vbox.addLayout(hbox)
285        text = QtWidgets.QTextEdit(panel)
286        hbox.addWidget(text)
287        text.insertHtml("""
288<style type="text/css">
289table { margin-left: 20px; }
290td { padding-left: 20px; }
291</style>
292<a href="#top"></a>
293<h1>IDeATe MQTT Monitor</h1>
294<p>This Python application is a tool intended for debugging programs which pass short data 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 publishing new messages on a specified message topic.</p>
295<h2>Connecting</h2>
296<p>The first set of controls configures server parameters before attempting a connection.  Changes will not take effect until the next connection attempt.</p
297
298<dl>
299  <dt>server address</dt><dd>The network name of the MQTT server. (Defaults to mqtt.ideate.cmu.edu.)</dd>
300  <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>
301  <dt>username</dt><dd>Server-specific identity, chosen by your instructor.</dd>
302  <dt>password</dt><dd>Server-specific password, chosen by your instructor.</dd>
303</dl>
304
305<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>
306
307<h2>Listening</h2>
308
309<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.
310Our 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>
311
312
313<p>
314<table>
315<tr><td><b>xyzzy</b></td><td>top-level topic on which user 'xyzzy' should publish</td></tr>
316<tr><td><b>xyzzy/status</b></td><td>a sub-topic on which user 'xyzzy' could publish</td></tr>
317<tr><td><b>xyzzy/sensor</b></td><td>another sub-topic on which user 'xyzzy' could publish</td></tr>
318<tr><td><b>xyzzy/sensor/1</b></td><td>a possible sub-sub-topic</td></tr>
319</table>
320</p>
321
322<p>The message subscription field specifies topics to receive.  The subscription may include a # character as a wildcard, as per the following examples.</p>
323<p><table>
324<tr><td><b>#</b></td><td>subscribe to all messages</td></tr>
325<tr><td><b>xyzzy</b></td><td>subscribe to the top-level published messages for user xyzzy</td></tr>
326<tr><td><b>xyzzy/#</b></td><td>subscribe to all published messages for user xyzzy, including subtopics</td></tr>
327</table>
328</p>
329<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>
330
331<p>The large text field is the console area which shows both debugging and status log messages as well as received messages.</p>
332
333<h2>Sending</h2>
334
335<p>At the bottom are a topic field and data field for publishing plain text messages.  Pressing enter in the data field will
336transmit the data string on the specified topic.  The text is not cleared after entry, so pressing enter multiple times will send the same text multiple times.</p>
337<p>The MQTT protocol supports binary messages (i.e. any sequence of bytes), but this tool currently only supports sending messages with plain text.</p>
338
339
340<h2>More Information</h2>
341
342<p>The IDeATE server has more detailed information on the server help page at <b>https://mqtt.ideate.cmu.edu</b></p>
343
344""")
345        text.scrollToAnchor("top")
346        text.setReadOnly(True)
347        panel.show()
348
349    def mqtt_server_name_entered(self):
350        name = self.mqtt_server_name.text()
351        self.write("Server name changed: %s" % name)
352        self.main.set_server_name(name)
353
354    def decode_port_selection(self):
355        title = self.port_selector.currentText()
356        if title == "":
357            return None
358        else:
359            return int(title.split()[0])  # convert the first token to a number
360
361    def mqtt_port_selected(self, title):
362        portnum  = self.decode_port_selection()
363        self.write("Port selection changed: %s" % title)
364        self.main.set_server_port(portnum)
365
366    def mqtt_username_entered(self):
367        name = self.mqtt_username.text()
368        self.write("User name changed: %s" % name)
369        self.main.set_username(name)
370
371    def mqtt_password_entered(self):
372        name = self.mqtt_password.text()
373        self.write("Password changed: %s" % name)
374        self.main.set_password(name)
375
376    def connection_requested(self):
377        # When the connect button is pressed, make sure all fields are up to
378        # date.  It is otherwise possible to leave a text field selected with
379        # unreceived changes while pressing Connect.
380        hostname = self.mqtt_server_name.text()
381        portnum  = self.decode_port_selection()
382        username = self.mqtt_username.text()
383        password = self.mqtt_password.text()
384
385        self.main.set_server_name(hostname)
386        self.main.set_server_port(portnum)
387        self.main.set_username(username)
388        self.main.set_password(password)
389
390        self.main.connect_to_mqtt_server()
391
392    def mqtt_sub_entered(self):
393        sub = self.mqtt_sub.text()
394        if sub == '':
395            self.mqtt_sub.setText("#")
396            sub = "#"
397
398        self.write("Subscription changed to: %s" % sub)
399        self.main.set_subscription(sub)
400
401    def mqtt_topic_entered(self):
402        topic = self.mqtt_topic.text()
403        self.write("Topic changed to: %s" % topic)
404        self.main.set_topic(topic)
405
406    def mqtt_payload_entered(self):
407        topic = self.mqtt_topic.text()
408        payload = self.mqtt_payload.text()
409        self.main.send_message(topic, payload)
410
411################################################################
412class MainApp(object):
413    """Main application object holding any non-GUI related state."""
414
415    def __init__(self):
416
417        # Attach a handler to the keyboard interrupt (control-C).
418        signal.signal(signal.SIGINT, self._sigint_handler)
419
420        # load any available persistent application settings
421        QtCore.QCoreApplication.setOrganizationName("IDeATe")
422        QtCore.QCoreApplication.setOrganizationDomain("ideate.cmu.edu")
423        QtCore.QCoreApplication.setApplicationName('mqtt_monitor')
424        self.settings = QtCore.QSettings()
425
426        # uncomment to restore 'factory defaults'
427        # self.settings.clear()
428
429        # MQTT server settings
430        self.hostname = self.settings.value('mqtt_host', 'mqtt.ideate.cmu.edu')
431        self.portnum  = self.settings.value('mqtt_port', None)
432        self.username = self.settings.value('mqtt_user', 'students')
433        self.password = self.settings.value('mqtt_password', '(not yet entered)')
434
435        # Create a default subscription based on the username.  The hash mark is a wildcard.
436        username = getpass.getuser()
437        self.subscription = self.settings.value('mqtt_subscription', username + '/#')
438
439        # default message to send
440        self.topic = self.settings.value('mqtt_topic', username)
441        self.payload = self.settings.value('mqtt_payload', 'hello')
442
443        # create the interface window
444        self.window = MainGUI(self)
445
446        # Initialize the MQTT client system
447        self.client = mqtt.Client()
448        self.client.enable_logger(mqtt_log)
449        self.client.on_log = self.on_log
450        self.client.on_connect = self.on_connect
451        self.client.on_disconnect = self.on_disconnect
452        self.client.on_message = self.on_message
453        self.client.tls_set()
454
455        self.window.show_status("Please set the MQTT server address and select Connect.")
456        return
457
458    ################################################################
459    def app_is_exiting(self):
460        if self.client.is_connected():
461            self.client.disconnect()
462            self.client.loop_stop()
463
464    def _sigint_handler(self, signal, frame):
465        print("Keyboard interrupt caught, running close handlers...")
466        self.app_is_exiting()
467        sys.exit(0)
468
469    ################################################################
470    def set_server_name(self, name):
471        self.hostname = name
472        self.settings.setValue('mqtt_host', name)
473
474    def set_server_port(self, value):
475        self.portnum = value
476        self.settings.setValue('mqtt_port', self.portnum)
477
478    def set_username(self, name):
479        self.username = name
480        self.settings.setValue('mqtt_user', name)
481
482    def set_password(self, name):
483        self.password = name
484        self.settings.setValue('mqtt_password', name)
485
486    def connect_to_mqtt_server(self):
487        if self.client.is_connected():
488            self.window.write("Already connected.")
489        else:
490            if self.portnum is None:
491                log.warning("Please specify the server port before attempting connection.")
492            else:
493                log.debug("Initiating MQTT connection to %s:%d" % (self.hostname, self.portnum))
494                self.window.write("Attempting connection.")
495                self.client.username_pw_set(self.username, self.password)
496                self.client.connect_async(self.hostname, self.portnum)
497                self.client.loop_start()
498
499    def disconnect_from_mqtt_server(self):
500        if self.client.is_connected():
501            self.client.disconnect()
502        else:
503            self.window.write("Not connected.")
504        self.client.loop_stop()
505
506    ################################################################
507    # The callback for when the broker responds to our connection request.
508    def on_connect(self, client, userdata, flags, rc):
509        self.window.write("Connected to server with with flags: %s, result code: %s" % (flags, rc))
510
511        if rc == 0:
512            log.info("Connection succeeded.")
513
514        elif rc > 0:
515            if rc < len(mqtt_rc_codes):
516                log.warning("Connection failed with error: %s", mqtt_rc_codes[rc])
517            else:
518                log.warning("Connection failed with unknown error %d", rc)
519
520        # Subscribing in on_connect() means that if we lose the connection and reconnect then subscriptions will be renewed.
521        client.subscribe(self.subscription)
522        self.window.show_status("Connected.")
523        self.window.set_connected_state(True)
524        return
525
526    # The callback for when the broker responds with error messages.
527    def on_log(client, userdata, level, buf):
528        log.debug("on_log level %s: %s", level, userdata)
529        return
530
531    def on_disconnect(self, client, userdata, rc):
532        log.debug("disconnected")
533        self.window.write("Disconnected from server.")
534        self.window.show_status("Disconnected.")
535        self.window.set_connected_state(False)
536
537    # The callback for when a message has been received on a topic to which this
538    # client is subscribed.  The message variable is a MQTTMessage that describes
539    # all of the message parameters.
540
541    # Some useful MQTTMessage fields: topic, payload, qos, retain, mid, properties.
542    #   The payload is a binary string (bytes).
543    #   qos is an integer quality of service indicator (0,1, or 2)
544    #   mid is an integer message ID.
545
546    def on_message(self, client, userdata, msg):
547        self.window.write("{%s} %s" % (msg.topic, msg.payload))
548        return
549
550    ################################################################
551    def set_subscription(self, sub):
552        if self.client.is_connected():
553            self.client.unsubscribe(self.subscription)
554            try:
555                self.client.subscribe(sub)
556                self.subscription = sub
557                self.settings.setValue('mqtt_subscription', sub)
558            except ValueError:
559                self.window.write("Invalid subscription string, not changed.")
560                self.client.subscribe(self.subscription)
561        else:
562            self.subscription = sub
563            self.settings.setValue('mqtt_subscription', sub)
564
565    def set_topic(self, sub):
566        self.topic = sub
567        self.settings.setValue('mqtt_topic', sub)
568
569    def send_message(self, topic, payload):
570        if self.client.is_connected():
571            self.client.publish(topic, payload)
572        else:
573            self.window.write("Not connected.")
574        self.payload = payload
575        self.settings.setValue('mqtt_payload', payload)
576
577    ################################################################
578
579def main():
580    # Optionally add an additional root log handler to stream messages to the terminal console.
581    if False:
582        console_handler = logging.StreamHandler()
583        console_handler.setLevel(logging.DEBUG)
584        console_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
585        logging.getLogger().addHandler(console_handler)
586
587    # initialize the Qt system itself
588    app = QtWidgets.QApplication(sys.argv)
589
590    # create the main application controller
591    main = MainApp()
592
593    # run the event loop until the user is done
594    log.info("Starting event loop.")
595    sys.exit(app.exec_())
596
597################################################################
598# Main script follows.  This sequence is executed when the script is initiated from the command line.
599
600if __name__ == "__main__":
601    main()