Arduino-MQTT Bridge (PyQt5)¶
This GUI application connects a microcontroller program to a remote MQTT server over the network. It communicates with the microcontroller using the serial port, relaying lines of text to and from the MQTT server. This can be used as a platform for remote collaboration between embedded devices.
The application is provided all in one file and can be be directly downloaded from qt_arduino_mqtt_bridge.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:
Install one of the remote connection examples on your CircuitPython board. These can serve as starting points for your own sketches.
Launch the
qt_arduino_mqtt_bridge.py
program using Python 3.Select your Arduino serial port from the dropdown, then click Connect.
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 your Andrew ID in the sending field; this will identify your transmitted messages.
Enter your partner Andrew ID in the receiving field; this will select incoming messages.
If testing locally, it is convenient also to run the MQTT Monitor (PyQt5) in order to simulate the collaborating system.
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¶
QtArduinoMQTT¶
- class mqtt.qt_arduino_mqtt_bridge.QtArduinoMQTT(main)[source]¶
Class to manage a serial connection to an Arduino MQTT sketch using Qt QSerialPort object for data transport. The data protocol is based on lines of text.
MainGUI¶
The verbose user interface code is separated into a separate class from the serial port and network logic.
Full Code¶
1#!/usr/bin/env python3
2"""A PyQt5 GUI utility to connect an Arduino with a remote MQTT server."""
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, QtSerialPort
22
23# documentation: https://www.eclipse.org/paho/clients/python/docs/
24import paho.mqtt.client as mqtt
25
26# configure logging output
27log = logging.getLogger('main')
28log.setLevel(logging.INFO)
29
30mqtt_log = logging.getLogger('mqtt')
31mqtt_log.setLevel(logging.INFO)
32
33paho_log = logging.getLogger('paho.mqtt')
34paho_log.setLevel(logging.INFO)
35
36# IDeATE server instances, as per https://mqtt.ideate.cmu.edu/#ports
37
38ideate_ports = { 8884 : '16-223',
39 8885 : '16-375',
40 8886 : '60-223',
41 8887 : '62-362',
42 8889 : '16-376',
43}
44mqtt_rc_codes = ['Success', 'Incorrect protocol version', 'Invalid client identifier', 'Server unavailable', 'Bad username or password', 'Not authorized']
45
46# Set the version identifier. This should be updated prior to setting a new git tag.
47version_id = 'v1.1'
48
49################################################################
50class QtArduinoMQTT(QtCore.QObject):
51 """Class to manage a serial connection to an Arduino MQTT sketch using Qt
52 QSerialPort object for data transport. The data protocol is based on lines
53 of text.
54 """
55
56 # class variable with Qt signal used to communicate between background thread and serial port thread
57 _threadedWrite = QtCore.pyqtSignal(bytes, name='threadedWrite')
58
59 def __init__(self, main):
60 super(QtArduinoMQTT,self).__init__()
61 self._portname = None
62 self._buffer = b''
63 self._port = None
64 self.log = logging.getLogger('arduino')
65 self.log.setLevel(logging.INFO)
66 self.main = main
67 return
68
69 def is_open(self):
70 return self._port is not None
71
72 def available_ports(self):
73 """Return a list of names of available serial ports."""
74 return [port.portName() for port in QtSerialPort.QSerialPortInfo.availablePorts()]
75
76 def set_port(self, name):
77 self._portname = name
78
79 def open(self):
80 """Open the serial port and initialize communications. If the port is already
81 open, this will close it first. If the current name is None, this will not open
82 anything. Returns True if the port is open, else False."""
83 if self._port is not None:
84 self.close()
85
86 if self._portname is None:
87 self.log.debug("No port name provided so not opening port.")
88 return False
89
90 self._port = QtSerialPort.QSerialPort()
91 self._port.setBaudRate(115200)
92 self._port.setPortName(self._portname)
93
94 # open the serial port, which should also reset the Arduino
95 if self._port.open(QtCore.QIODevice.ReadWrite):
96 self.log.info("Opened serial port %s", self._port.portName())
97 # always process data as it becomes available
98 self._port.readyRead.connect(self.read_input)
99
100 # initialize the slot used to receive data from background threads
101 self._threadedWrite.connect(self._data_send)
102
103 return True
104
105 else:
106 # Error codes: https://doc.qt.io/qt-5/qserialport.html#SerialPortError-enum
107 errcode = self._port.error()
108 if errcode == QtSerialPort.QSerialPort.PermissionError:
109 self.log.warning("Failed to open serial port %s with a QSerialPort PermissionError, which could involve an already running control process, a stale lock file, or dialout group permissions.", self._port.portName())
110 else:
111 self.log.warning("Failed to open serial port %s with a QSerialPort error code %d.", self._port.portName(), errcode)
112 self._port = None
113 return False
114
115 def set_and_open_port(self, name):
116 self.set_port(name)
117 self.open()
118
119 def close(self):
120 """Shut down the serial connection to the Arduino."""
121 if self._port is not None:
122 self.log.info("Closing serial port %s", self._port.portName())
123 self._port.close()
124 self._port = None
125 return
126
127 def write(self, data):
128 if self._port is not None:
129 self._port.write(data)
130 else:
131 self.log.debug("Serial port not open during write.")
132
133 @QtCore.pyqtSlot(bytes)
134 def _data_send(self, data):
135 """Slot to receive serial data on the main thread."""
136 self.write(data)
137
138 def thread_safe_write(self, data):
139 """Function to receive data to transmit from a background thread, then send it as a signal to a slot on the main thread."""
140 self._threadedWrite.emit(data)
141
142 def read_input(self):
143 # Read as much input as available; callback from Qt event loop.
144 data = self._port.readAll()
145 if len(data) > 0:
146 self.data_received(data)
147 return
148
149 def _parse_serial_input(self, data):
150 # parse a single line of status input provided as a bytestring
151 tokens = data.split()
152 self.log.debug("Received serial data: %s", tokens)
153 self.main.send_arduino_message(data)
154
155 def data_received(self, data):
156 # Manage the possibility of partial reads by appending new data to any previously received partial line.
157 # The data arrives as a PyQT5.QtCore.QByteArray.
158 self._buffer += bytes(data)
159
160 # Process all complete newline-terminated lines.
161 while b'\n' in self._buffer:
162 first, self._buffer = self._buffer.split(b'\n', 1)
163 first = first.rstrip()
164 self._parse_serial_input(first)
165
166 def send(self, string):
167 self.log.debug("Sending to serial port: %s", string)
168 self.write(string.encode()+b'\n')
169 return
170
171
172################################################################
173class MainGUI(QtWidgets.QMainWindow):
174 """A custom main window which provides all GUI controls. Requires a delegate main application object to handle user requests."""
175
176 def __init__(self, main, *args, **kwargs):
177 super(MainGUI,self).__init__()
178
179 # save the main object for delegating GUI events
180 self.main = main
181
182 # create the GUI elements
183 self.console_queue = queue.Queue()
184 self.setupUi()
185
186 self._handler = None
187 self.enable_console_logging()
188
189 # finish initialization
190 self.show()
191
192 # manage the console output across threads
193 self.console_timer = QtCore.QTimer()
194 self.console_timer.timeout.connect(self._poll_console_queue)
195 self.console_timer.start(50) # units are milliseconds
196
197 return
198
199 # ------------------------------------------------------------------------------------------------
200 def setupUi(self):
201 self.setWindowTitle("IDeATe Arduino-MQTT Bridge " + version_id)
202 self.resize(600, 800)
203
204 self.centralwidget = QtWidgets.QWidget(self)
205 self.setCentralWidget(self.centralwidget)
206 self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
207 self.verticalLayout.setContentsMargins(-1, -1, -1, 9) # left, top, right, bottom
208
209 # help panel button
210 help = QtWidgets.QPushButton('Open the Help Panel')
211 help.pressed.connect(self.help_requested)
212 self.verticalLayout.addWidget(help)
213
214 hrule = QtWidgets.QFrame()
215 hrule.setFrameShape(QtWidgets.QFrame.HLine)
216 self.verticalLayout.addWidget(hrule)
217
218 # generate fields for configuring the Arduino serial port
219 hbox = QtWidgets.QHBoxLayout()
220 self.verticalLayout.addLayout(hbox)
221 hbox.addWidget(QtWidgets.QLabel("Arduino serial port:"))
222 self.portSelector = QtWidgets.QComboBox()
223 hbox.addWidget(self.portSelector)
224 self.update_port_selector()
225 self.portSelector.activated['QString'].connect(self.arduino_port_selected)
226
227 rescan = QtWidgets.QPushButton('Rescan Serial Ports')
228 rescan.pressed.connect(self.update_port_selector)
229 hbox.addWidget(rescan)
230
231 # Arduino connection indicator and connect/disconnect buttons
232 hbox = QtWidgets.QHBoxLayout()
233 self.verticalLayout.addLayout(hbox)
234 self.arduino_connected = QtWidgets.QLabel()
235 self.arduino_connected.setLineWidth(3)
236 self.arduino_connected.setFrameStyle(QtWidgets.QFrame.Box)
237 self.arduino_connected.setAlignment(QtCore.Qt.AlignCenter)
238 sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
239 self.arduino_connected.setSizePolicy(sizePolicy)
240 self.set_arduino_connected_state(False)
241 hbox.addWidget(self.arduino_connected)
242 connect = QtWidgets.QPushButton('Connect')
243 connect.pressed.connect(self.main.connect_to_arduino)
244 hbox.addWidget(connect)
245 disconnect = QtWidgets.QPushButton('Disconnect')
246 disconnect.pressed.connect(self.main.disconnect_from_arduino)
247 hbox.addWidget(disconnect)
248
249 hrule = QtWidgets.QFrame()
250 hrule.setFrameShape(QtWidgets.QFrame.HLine)
251 self.verticalLayout.addWidget(hrule)
252
253 # generate GUI for configuring the MQTT connection
254
255 # server name entry and port selection
256 hbox = QtWidgets.QHBoxLayout()
257 self.verticalLayout.addLayout(hbox)
258 hbox.addWidget(QtWidgets.QLabel("MQTT server address:"))
259 self.mqtt_server_name = QtWidgets.QLineEdit()
260 self.mqtt_server_name.setText(str(self.main.hostname))
261 self.mqtt_server_name.editingFinished.connect(self.mqtt_server_name_entered)
262 hbox.addWidget(self.mqtt_server_name)
263
264 hbox.addWidget(QtWidgets.QLabel("port:"))
265 self.port_selector = QtWidgets.QComboBox()
266 hbox.addWidget(self.port_selector)
267
268 self.port_selector.addItem("")
269 for pairs in ideate_ports.items():
270 self.port_selector.addItem("%d (%s)" % pairs)
271 self.port_selector.activated['QString'].connect(self.mqtt_port_selected)
272
273 # attempt to pre-select the stored port number
274 try:
275 idx = list(ideate_ports.keys()).index(self.main.portnum)
276 self.port_selector.setCurrentIndex(idx+1)
277 except ValueError:
278 pass
279
280 # instructions
281 explanation = QtWidgets.QLabel("""Username and password provided by instructor. Please see help panel for details.""")
282 explanation.setWordWrap(True)
283 self.verticalLayout.addWidget(explanation)
284
285 # user and password entry
286 hbox = QtWidgets.QHBoxLayout()
287 self.verticalLayout.addLayout(hbox)
288 hbox.addWidget(QtWidgets.QLabel("MQTT username:"))
289 self.mqtt_username = QtWidgets.QLineEdit()
290 self.mqtt_username.setText(str(self.main.username))
291 self.mqtt_username.editingFinished.connect(self.mqtt_username_entered)
292 hbox.addWidget(self.mqtt_username)
293
294 hbox.addWidget(QtWidgets.QLabel("password:"))
295 self.mqtt_password = QtWidgets.QLineEdit()
296 self.mqtt_password.setText(str(self.main.password))
297 self.mqtt_password.editingFinished.connect(self.mqtt_password_entered)
298 hbox.addWidget(self.mqtt_password)
299
300 # connection indicator
301 self.mqtt_connected = QtWidgets.QLabel()
302 self.mqtt_connected.setLineWidth(3)
303 self.mqtt_connected.setFrameStyle(QtWidgets.QFrame.Box)
304 self.mqtt_connected.setAlignment(QtCore.Qt.AlignCenter)
305 sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
306 self.mqtt_connected.setSizePolicy(sizePolicy)
307 self.set_mqtt_connected_state(False)
308
309 # connection control buttons
310 connect = QtWidgets.QPushButton('Connect')
311 connect.pressed.connect(self.connection_requested)
312 disconnect = QtWidgets.QPushButton('Disconnect')
313 disconnect.pressed.connect(self.main.disconnect_from_mqtt_server)
314 hbox = QtWidgets.QHBoxLayout()
315 hbox.addWidget(self.mqtt_connected)
316 hbox.addWidget(connect)
317 hbox.addWidget(disconnect)
318 self.verticalLayout.addLayout(hbox)
319
320 hrule = QtWidgets.QFrame()
321 hrule.setFrameShape(QtWidgets.QFrame.HLine)
322 self.verticalLayout.addWidget(hrule)
323
324 # user and partner ID instructions
325 explanation = QtWidgets.QLabel("""Andrew IDs identify where to send and receive messages. Please see help panel for details.""")
326 explanation.setWordWrap(True)
327 self.verticalLayout.addWidget(explanation)
328
329 # user ID entry
330 hbox = QtWidgets.QHBoxLayout()
331 hbox.addWidget(QtWidgets.QLabel("Your Andrew ID (for sending):"))
332 self.user_id = QtWidgets.QLineEdit()
333 self.user_id.setText(self.main.user_id)
334 self.user_id.editingFinished.connect(self.user_id_entered)
335 hbox.addWidget(self.user_id)
336 self.verticalLayout.addLayout(hbox)
337
338 # partner ID entry
339 hbox = QtWidgets.QHBoxLayout()
340 hbox.addWidget(QtWidgets.QLabel("Partner's Andrew ID (for receiving):"))
341 self.partner_id = QtWidgets.QLineEdit()
342 self.partner_id.setText(self.main.partner_id)
343 self.partner_id.editingFinished.connect(self.partner_id_entered)
344 hbox.addWidget(self.partner_id)
345 self.verticalLayout.addLayout(hbox)
346
347 # text area for displaying both internal and received messages
348 self.consoleOutput = QtWidgets.QPlainTextEdit()
349 self.consoleOutput.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
350 self.verticalLayout.addWidget(self.consoleOutput)
351
352 # instructions
353 explanation = QtWidgets.QLabel("""Entering text below will manually send a message.""")
354 explanation.setWordWrap(True)
355 self.verticalLayout.addWidget(explanation)
356
357 # message payload entry
358 hbox = QtWidgets.QHBoxLayout()
359 label = QtWidgets.QLabel("User message:")
360 self.mqtt_payload = QtWidgets.QLineEdit()
361 self.mqtt_payload.setText(self.main.payload)
362 self.mqtt_payload.returnPressed.connect(self.mqtt_payload_entered)
363 hbox.addWidget(label)
364 hbox.addWidget(self.mqtt_payload)
365 self.verticalLayout.addLayout(hbox)
366
367 # set up the status bar which appears at the bottom of the window
368 self.statusbar = QtWidgets.QStatusBar(self)
369 self.setStatusBar(self.statusbar)
370
371 # set up the main menu
372 self.menubar = QtWidgets.QMenuBar(self)
373 self.menubar.setGeometry(QtCore.QRect(0, 0, 500, 22))
374 self.menubar.setNativeMenuBar(False)
375 self.menubar.setObjectName("menubar")
376 self.menuTitle = QtWidgets.QMenu(self.menubar)
377 self.setMenuBar(self.menubar)
378 self.actionQuit = QtWidgets.QAction(self)
379 self.menuTitle.addAction(self.actionQuit)
380 self.menubar.addAction(self.menuTitle.menuAction())
381 self.menuTitle.setTitle("File")
382 self.actionQuit.setText("Quit")
383 self.actionQuit.setShortcut("Ctrl+Q")
384 self.actionQuit.triggered.connect(self.quitSelected)
385
386 return
387
388 # --- logging to screen -------------------------------------------------------------
389 def enable_console_logging(self):
390 # get the root logger to receive all logging traffic
391 logger = logging.getLogger()
392 # create a logging handler which writes to the console window via self.write
393 handler = logging.StreamHandler(self)
394 handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
395 logger.addHandler(handler)
396 # logger.setLevel(logging.NOTSET)
397 logger.setLevel(logging.DEBUG)
398 handler.setLevel(logging.DEBUG)
399 self._handler = handler
400 log.info("Enabled logging in console window.")
401 return
402
403 def disable_console_logging(self):
404 if self._handler is not None:
405 logging.getLogger().removeHandler(self._handler)
406 self._handler = None
407
408 # --- window and qt event processing -------------------------------------------------------------
409 def show_status(self, string):
410 self.statusbar.showMessage(string)
411
412 def _poll_console_queue(self):
413 """Write any queued console text to the console text area from the main thread."""
414 while not self.console_queue.empty():
415 string = str(self.console_queue.get())
416 stripped = string.rstrip()
417 if stripped != "":
418 self.consoleOutput.appendPlainText(stripped)
419 return
420
421 def write(self, string):
422 """Write output to the console text area in a thread-safe way. Qt only allows
423 calls from the main thread, but the service routines run on separate threads."""
424 self.console_queue.put(string)
425 return
426
427 def quitSelected(self):
428 self.write("User selected quit.")
429 self.close()
430
431 def closeEvent(self, event):
432 self.write("Received window close event.")
433 self.main.app_is_exiting()
434 self.disable_console_logging()
435 super(MainGUI,self).closeEvent(event)
436
437 def set_mqtt_connected_state(self, flag):
438 if flag is True:
439 self.mqtt_connected.setText(" Connected ")
440 self.mqtt_connected.setStyleSheet("color: white; background-color: green;")
441 else:
442 self.mqtt_connected.setText(" Not Connected ")
443 self.mqtt_connected.setStyleSheet("color: white; background-color: blue;")
444
445 def set_arduino_connected_state(self, flag):
446 if flag is True:
447 self.arduino_connected.setText(" Connected ")
448 self.arduino_connected.setStyleSheet("color: white; background-color: green;")
449 else:
450 self.arduino_connected.setText(" Not Connected ")
451 self.arduino_connected.setStyleSheet("color: white; background-color: blue;")
452
453 def update_port_selector(self):
454 self.portSelector.clear()
455 self.portSelector.addItem("<no port selected>")
456 for port in QtSerialPort.QSerialPortInfo.availablePorts():
457 self.portSelector.insertItem(0, port.portName())
458 self.portSelector.setCurrentText(self.main.portname)
459
460 # --- GUI widget event processing ----------------------------------------------------------------------
461 def help_requested(self):
462 panel = QtWidgets.QDialog(self)
463 panel.resize(600,400)
464 panel.setWindowTitle("IDeATe MQTT Monitor: Help Info")
465 vbox = QtWidgets.QVBoxLayout(panel)
466 hbox = QtWidgets.QHBoxLayout()
467 vbox.addLayout(hbox)
468 text = QtWidgets.QTextEdit(panel)
469 hbox.addWidget(text)
470 text.insertHtml("""
471<style type="text/css">
472table { margin-left: 20px; }
473td { padding-left: 20px; }
474</style>
475<a href="#top"></a>
476<h1>IDeATe Arduino-MQTT Bridge</h1>
477
478<p>This Python application is a tool intended for connecting an Arduino sketch
479to a remote Arduino by passing short data messages back and forth across the
480network via a MQTT server. It supports connecting to an Arduino serial port,
481opening an authenticated connection to a remote server, subscribing to a class of
482messages in order to receive them, viewing message traffic, and publishing new
483messages from the Arduino on a specified message topic.</p>
484
485<p>In typical use, the messages are readable text such as lines of numbers
486separated by spaces. The Arduino serial data is treated as line-delimited text,
487with each text line input transmitted as a message. I.e., messages can be sent
488to the remote Arduino using <code>Serial.println()</code>. Received messages
489are treated as text and forwarded to the Arduino with an appended linefeed
490character. Sketches processing the received network data may use Serial.read()
491and related functions to parse the input.</p>
492
493<h2>Arduino</h2>
494
495<p>The first set of controls configures the Arduino connection. The port names
496are only scanned during launch so the application will need to be restarted if
497the Arduino is plugged in or moved after starting. Please note this is the raw
498list of serial ports which will likely include other non-Arduino devices; when
499in doubt, please use the same port name used by the Arduino IDE.</p>
500
501<p>The Arduino USB serial port is shared with the Arduino IDE, so you may need
502to disconnect the Arduino IDE Console or stop the Arduino IDE entirely for the
503connection to succeed. You'll also need to disconnect this tool before
504reloading code with the IDE.</p>
505
506<h2>Connecting to MQTT</h2>
507
508<p>The next set of controls configures server parameters before attempting a
509connection. Changes will not take effect until the next connection attempt.</p
510
511<dl>
512 <dt>server address</dt><dd>The network name of the MQTT server. (Defaults to mqtt.ideate.cmu.edu.)</dd>
513 <dt>server port</dt><dd>The numeric port number for the MQTT server. IDeATe is
514 using a separate server for each course, so the drop-down menu also identifies the associated course number.</dd>
515 <dt>username</dt><dd>Server-specific identity, chosen by your instructor.</dd>
516 <dt>password</dt><dd>Server-specific password, chosen by your instructor.</dd>
517</dl>
518
519<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>
520
521<h2>User Identification and Messages</h2>
522
523<p>MQTT works on a publish/subscribe model in which messages are published on
524<i>topics</i> identified by a topic name. For simplicity, this tool publishes your
525Arduino output on a single topic based on your Andrew ID; anyone on the server may read this topic to receive your data.
526Similarly, this tool <i>subscribes</i> (i.e. listens) to a single topic based on your partner's Andrew ID.
527<p>
528
529<p>Changing either the user or partner ID immediately changes what is sent or
530received.</p>
531
532<p>The large text field below the ID fieldss is the console area which shows
533message traffic as well as status and debugging messages.</p>
534
535<p>At the bottom is a field for publishing a message as if it were received from
536your Arduino.<p>
537
538<h2>More Information</h2>
539
540<p>The IDeATE server has more detailed information on the server help page at <b>https://mqtt.ideate.cmu.edu</b></p>
541
542""")
543 text.scrollToAnchor("top")
544 text.setReadOnly(True)
545 panel.show()
546
547 def arduino_port_selected(self, name):
548 self.write("Arduino port selected: %s" % name)
549 self.main.set_arduino_port(name)
550
551 def mqtt_server_name_entered(self):
552 name = self.mqtt_server_name.text()
553 self.write("Server name changed: %s" % name)
554 self.main.set_server_name(name)
555
556 def decode_port_selection(self):
557 title = self.port_selector.currentText()
558 if title == "":
559 return None
560 else:
561 return int(title.split()[0]) # convert the first token to a number
562
563 def mqtt_port_selected(self, title):
564 portnum = self.decode_port_selection()
565 self.write("Port selection changed: %s" % title)
566 self.main.set_server_port(portnum)
567
568 def mqtt_username_entered(self):
569 name = self.mqtt_username.text()
570 self.write("User name changed: %s" % name)
571 self.main.set_username(name)
572
573 def mqtt_password_entered(self):
574 name = self.mqtt_password.text()
575 self.write("Password changed: %s" % name)
576 self.main.set_password(name)
577
578 def connection_requested(self):
579 # When the connect button is pressed, make sure all fields are up to
580 # date. It is otherwise possible to leave a text field selected with
581 # unreceived changes while pressing Connect.
582 hostname = self.mqtt_server_name.text()
583 portnum = self.decode_port_selection()
584 username = self.mqtt_username.text()
585 password = self.mqtt_password.text()
586
587 self.main.set_server_name(hostname)
588 self.main.set_server_port(portnum)
589 self.main.set_username(username)
590 self.main.set_password(password)
591
592 self.main.connect_to_mqtt_server()
593
594 def user_id_entered(self):
595 id = self.user_id.text()
596 if id == '':
597 log.warning("User ID cannot be empty.")
598 else:
599 self.main.set_user_id(id)
600 self.write("Set user ID to %s" % id)
601
602 def partner_id_entered(self):
603 id = self.partner_id.text()
604 if id == '':
605 log.warning("Partner ID cannot be empty.")
606 else:
607 self.main.set_partner_id(id)
608 self.write("Set partner ID to %s" % id)
609
610 def mqtt_payload_entered(self):
611 payload = self.mqtt_payload.text()
612 self.main.send_message(payload)
613 self.mqtt_payload.clear()
614
615################################################################
616class MainApp(object):
617 """Main application object holding any non-GUI related state."""
618
619 def __init__(self):
620
621 # Attach a handler to the keyboard interrupt (control-C).
622 signal.signal(signal.SIGINT, self._sigint_handler)
623
624 # load any available persistent application settings
625 QtCore.QCoreApplication.setOrganizationName("IDeATe")
626 QtCore.QCoreApplication.setOrganizationDomain("ideate.cmu.edu")
627 QtCore.QCoreApplication.setApplicationName('arduino_mqtt_bridge')
628 self.settings = QtCore.QSettings()
629
630 # uncomment to restore 'factory defaults'
631 # self.settings.clear()
632
633 # Arduino serial port name
634 self.portname = self.settings.value('arduino_port', '')
635
636 # MQTT server settings
637 self.hostname = self.settings.value('mqtt_host', 'mqtt.ideate.cmu.edu')
638 self.portnum = self.settings.value('mqtt_port', None)
639 self.username = self.settings.value('mqtt_user', 'students')
640 self.password = self.settings.value('mqtt_password', '(not yet entered)')
641
642 # Student and partner Andrew IDs, used for generating send and receive message topics.
643 username = getpass.getuser()
644 self.user_id = self.settings.value('user_id', username)
645 self.partner_id = self.settings.value('partner_id', 'unspecified')
646
647 # Create a default subscription and topic based on the username.
648 self.subscription = self.partner_id
649 self.topic = self.user_id
650 self.payload = ''
651
652 # create the interface window
653 self.window = MainGUI(self)
654
655 # Initialize the MQTT client system
656 self.client = mqtt.Client()
657 self.client.enable_logger(paho_log)
658 self.client.on_log = self.on_log
659 self.client.on_connect = self.on_connect
660 self.client.on_disconnect = self.on_disconnect
661 self.client.on_message = self.on_message
662 self.client.tls_set()
663
664 # Initialize the Arduino interface system.
665 self.arduino = QtArduinoMQTT(self)
666 self.arduino.set_port(self.portname)
667
668 self.window.show_status("Disconnected.")
669 return
670
671 ################################################################
672 def app_is_exiting(self):
673 if self.client.is_connected():
674 self.client.disconnect()
675 self.client.loop_stop()
676 self.arduino.close()
677
678 def _sigint_handler(self, signal, frame):
679 print("Keyboard interrupt caught, running close handlers...")
680 self.app_is_exiting()
681 sys.exit(0)
682
683 ################################################################
684 def set_arduino_port(self, name):
685 self.settings.setValue('arduino_port', name)
686 self.portname = name
687 self.arduino.set_port(name)
688 return
689
690 def connect_to_arduino(self):
691 connected_flag = self.arduino.open()
692 self.window.set_arduino_connected_state(connected_flag)
693 return
694
695 def disconnect_from_arduino(self):
696 self.arduino.close()
697 self.window.set_arduino_connected_state(False)
698 return
699
700 ################################################################
701 def set_server_name(self, name):
702 self.hostname = name
703 self.settings.setValue('mqtt_host', name)
704
705 def set_server_port(self, value):
706 self.portnum = value
707 self.settings.setValue('mqtt_port', self.portnum)
708
709 def set_username(self, name):
710 self.username = name
711 self.settings.setValue('mqtt_user', name)
712
713 def set_password(self, name):
714 self.password = name
715 self.settings.setValue('mqtt_password', name)
716
717 def connect_to_mqtt_server(self):
718 if self.client.is_connected():
719 self.window.write("Already connected.")
720 else:
721 if self.portnum is None:
722 log.warning("Please specify the server port before attempting connection.")
723 else:
724 log.debug("Initiating MQTT connection to %s:%d" % (self.hostname, self.portnum))
725 self.window.write("Attempting connection.")
726 self.client.username_pw_set(self.username, self.password)
727 self.client.connect_async(self.hostname, self.portnum)
728 self.client.loop_start()
729
730 def disconnect_from_mqtt_server(self):
731 if self.client.is_connected():
732 self.client.disconnect()
733 else:
734 self.window.write("Not connected.")
735 self.client.loop_stop()
736
737 ################################################################
738 # The callback for when the broker responds to our connection request.
739 def on_connect(self, client, userdata, flags, rc):
740 mqtt_log.debug("Connected to server with with flags: %s, result code: %s", flags, rc)
741
742 if rc == 0:
743 mqtt_log.info("Connection succeeded.")
744
745 elif rc > 0:
746 if rc < len(mqtt_rc_codes):
747 mqtt_log.warning("Connection failed with error: %s", mqtt_rc_codes[rc])
748 else:
749 mqtt_log.warning("Connection failed with unknown error %d", rc)
750
751 # Subscribing in on_connect() means that if we lose the connection and reconnect then subscriptions will be renewed.
752 client.subscribe(self.subscription)
753 self.window.show_status("Connected.")
754 self.window.set_mqtt_connected_state(True)
755 return
756
757 # The callback for when the broker responds with error messages.
758 def on_log(client, userdata, level, buf):
759 mqtt_log.debug("level %s: %s", level, userdata)
760 return
761
762 def on_disconnect(self, client, userdata, rc):
763 mqtt_log.info("Disconnected from server.")
764 self.window.show_status("Disconnected.")
765 self.window.set_mqtt_connected_state(False)
766
767 # The callback for when a message has been received on a topic to which this
768 # client is subscribed. The message variable is a MQTTMessage that describes
769 # all of the message parameters.
770
771 # Some useful MQTTMessage fields: topic, payload, qos, retain, mid, properties.
772 # The payload is a binary string (bytes).
773 # qos is an integer quality of service indicator (0,1, or 2)
774 # mid is an integer message ID.
775
776 def on_message(self, client, userdata, msg):
777 printable = self._printable_message_text(msg.payload)
778 self.window.write("Received from %s: %s" % (msg.topic, printable))
779
780 if self.arduino.is_open():
781 line = msg.payload + b'\n'
782 log.debug("Sending received message to Arduino: %s", line)
783 self.arduino.thread_safe_write(line)
784 log.debug("Sent received message to Arduino: %s", line)
785 else:
786 log.debug("Arduino not open to receive message: %s", msg.payload)
787 return
788
789 def _printable_message_text(self, msg):
790 # Test whether the bytes array is 7-bit clean text, else binary, and provide a printable form.
791 if all(c > 31 and c < 128 for c in msg):
792 return msg.decode(encoding='ascii')
793 else:
794 return "binary message:" + str([c for c in msg])
795
796 ################################################################
797 def set_subscription(self, sub):
798 if self.client.is_connected():
799 self.client.unsubscribe(self.subscription)
800 try:
801 self.client.subscribe(sub)
802 self.subscription = sub
803 except ValueError:
804 self.window.write("Invalid subscription string, not changed.")
805 self.client.subscribe(self.subscription)
806 else:
807 self.subscription = sub
808
809 def set_user_id(self, id):
810 self.user_id = id
811 self.topic = id
812
813 def set_partner_id(self, id):
814 self.partner_id = id
815 self.set_subscription(id)
816
817 def send_message(self, payload):
818 """Publish a message entered by the user."""
819
820 if self.client.is_connected():
821 self.client.publish(self.topic, payload)
822 self.window.write("Transmitting on %s: %s" % (self.topic, payload))
823 else:
824 self.window.write("Not connected.")
825 self.payload = payload
826
827 def send_arduino_message(self, payload):
828 """Transfer a message from the Arduino to the current topic."""
829 if self.client.is_connected():
830 self.client.publish(self.topic, payload)
831 log.debug("Published Arduino message: %s", payload)
832 printable = self._printable_message_text(payload)
833 self.window.write("Transmitting on %s: %s" % (self.topic, printable))
834
835 ################################################################
836
837def main():
838 # Optionally add an additional root log handler to stream messages to the terminal console.
839 if False:
840 console_handler = logging.StreamHandler()
841 console_handler.setLevel(logging.DEBUG)
842 console_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
843 logging.getLogger().addHandler(console_handler)
844
845 # initialize the Qt system itself
846 app = QtWidgets.QApplication(sys.argv)
847
848 # create the main application controller
849 main = MainApp()
850
851 # run the event loop until the user is done
852 log.info("Starting event loop.")
853 sys.exit(app.exec_())
854
855################################################################
856# Main script follows. This sequence is executed when the script is initiated from the command line.
857
858if __name__ == "__main__":
859 main()