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 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()