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.
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:
Launch the
qt_mqtt_monitor.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. The default
#
will show all traffic. A pattern such asusername/#
will limit displayed traffic to a particular sender.Enter a transmission topic according to your needs. By convention we are using Andrew IDs to identify our own transmitted messages.
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¶
MainGUI¶
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()