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