This follows a similar idea to my second project, but takes it in a somewhat different direction, fully integrating it into my home network using the MQTT protocol. For those interested in what MQTT is, have a look here.

Images

Here are the three PC nodes in the network…

Jarvis (Iron Man reference), which is the system I usually work at…

Phoenix, which was the codename for this laptop when it was being developed. It sits in the same room as Jarvis, so their temps have effects on each other…

… and Tars (Interstellar reference). This hosts a Minecraft server and resides in my basement… quite dusty at the moment thanks to the kitchen renovation taking place directly above…

Here’s the the Arduino-powered gateway, which can be connected to any PC to interface with the Arduino MQTT bridge. It has been repurposed from the previous project.

The front of the device…

The rear of the device…

And here are a shot of the network in action…

Here’s a shot of the 3 PC nodes interacting with each other. Right now, they’re all running great, sending each other encouraging messages.

And here’s a shot of what might happen if you neglect your own personal health too much, leading the PCs to go on-strike…

While this is just “Wizard of Ozzed” for the sake of having a quick and dirty example (as well as some humor in my choice of commands…), this is something that can happen if you don’t take care of yourself.

Description

There’s quite a bit to cover here, so bear with me.

As I had stated in the briefing for my previous project, I tend to take much better care of my computers than I do myself (especially when it comes to sleep schedule). The previous idea was one way of attempting to remedy this, but it had some shortcomings. Namely, it was far too easy to game the system with regard to reporting temperatures of the systems, and it had no connection to the systems in order to assist in dealing with certain situations (i.e., when a system is running too hot, what do you do?).

The goal is to allow any and all nodes in the network to communicate their vitals to each other. There are 3 PC nodes (all running Manjaro Linux), as well as a human node. This human node consists of an Arduino (for capturing my vitals at any given moment using the same sensors as in the last project), as well as a PC for reading the output of the Arduino and sending it to the other nodes in the network via the MQTT protocol.

Most of the time, the systems simply communicate their vitals with each other and send each other little messages based on what may be happening at that moment. For instance, one PC may be under heavy load, but its temperatures are in-check. The other PCs will see that and send a message to spur it on further–something along the lines of, “Hey, you’re doing great!” (because especially in times like these, we can all use a little bit of affirmation).

In another instance, a PC may be under very little load, but the temps could still be high. This would indicate a serious problem with that machine (perhaps a fan failure or serious heat in the room). This would cause the other PCs in the network to “sound the alarm” and notify me that something is seriously wrong.

Certain audible responses are triggered based on what the situation is. For instance, if a PC is “sounding the alarm,” it will ring the terminal bell and say something along the lines of “HELP! I need somebody! Not just anybody!” (in a text-to-speech voice for added comedic effect).

Now, that only covers PC-to-PC interaction. At any given point, I can enter the network and report my own vitals via the Arduino connected to a PC. Like with the last project, these are Galvanic Skin Response/Resistance (GSR), heart rate, when I went to sleep the previous night, and for how long I slept. If my GSR is too low or heart rate is too high (indicating that I am stressed), then the PCs will see this and offer words of encouragement.

On the other hand, if I went to sleep at an unreasonable hour the previous night, then that will count as a strike. Strikes last until the end of the week. If I accumulate three strikes, then the computers go on-strike and refuse to perform certain actions. Pulling an all-nighter results in an automatic three strikes.

Progress

One of the things that required quite a bit of trial and error was pulling the necessary data from terminal output. I was simply capturing the output of the sensors command (to get the CPU temperature) and nvidia-smi (to get GPU temperature and utilization, since I have Nvidia GPUs in my systems.

Initially, when the GPU was under 100% load, it would read as 00. This was an indexing issue and was easy to fix.

This happened for no apparent reason. It’s possible that the program just received garbage data from nvidia-smi…

This didn’t go over well with my parents… more on that later…

Since there wasn’t a bunch to do on the Arduino side of things and most of my issues were in Python, that’s all I have for in-progress photo documentation, sadly.

Process Reflection

As with everything I’ve done thus far, time management was a major issue. I’m going to save the details on that, though, as I’ve gone into it already in documentation for my previous works.

Getting the PCs to talk to each other over MQTT was far easier than I had anticipated thanks to Garth Zeglin’s work on the MQTT bridge and server for IDeATe. He did the majority of the dirty work, and there was no need to reinvent the wheel, so I borrowed liberally from his original code.

Something much more difficult was getting the system to consistently read the same sensor data. I’m not sure why, but occasionally, nvidia-smi will output garbage for the GPU utilization. This led to crashes that were initially very hard to track down, but after a while, I simply added an empty string check where necessary–the crashes are gone now.

Another difficult part was figuring out what to actually do based on certain scenarios. I wanted to have a somewhat whimsical feel to the PC reactions regardless of what the situation was. Having heard RZach use a somewhat comical text-to-speech voice during class throughout the semester to notify us when our presentation time slot was ending, I decided to follow a similar path and have the machines output responses to me via text-to-speech.

Unfortunately, that did not go over well with my parents when they heard Tars say “Seth, what the hell are you doing? I’m burning over here!” in a robotic voice… right in the middle of their Netflix-watching time. As a result, that code has been omitted for the time being.

Perhaps when I get my own place, I can revisit that. For now, I have to stick with more discrete notifications like plain text messages over MQTT.

Code

Here are the scripts that run on the 3 PC nodes. They are all very similar, but have minor differences to account for Intel vs. AMD CPU (different way of reporting temps), as well as GPU vitals reporting.

For Jarvis…

#!/usr/bin/env python3
"""A PyQt5 GUI utility to monitor and send MQTT server messages."""

################################################################
# Written in 2018-2020 by Garth Zeglin <garthz@cmu.edu>

# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.

# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.

################################################################
# standard Python libraries
from __future__ import print_function
import os, sys, struct, time, logging, functools, queue, signal, getpass, psutil

# documentation: https://doc.qt.io/qt-5/index.html
# documentation: https://www.riverbankcomputing.com/static/Docs/PyQt5/index.html
from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork

# documentation: https://www.eclipse.org/paho/clients/python/docs/
import paho.mqtt.client as mqtt

# default logging output
log = logging.getLogger('main')

# logger to pass to the MQTT library
mqtt_log = logging.getLogger('mqtt')
mqtt_log.setLevel(logging.WARNING)

# IDeATE server instances, as per https://mqtt.ideate.cmu.edu/#ports

ideate_ports = { 8884 : '16-223',
                 8885 : '16-375',
                 8886 : '60-223',
                 8887 : '62-362',
}

mqtt_rc_codes = ['Success', 'Incorrect protocol version', 'Invalid client identifier', 'Server unavailable', 'Bad username or password', 'Not authorized']
counter = 0


################################################################
class MainApp(object):
    """Main application object holding any non-GUI related state."""

    def __init__(self):

        # Attach a handler to the keyboard interrupt (control-C).
        signal.signal(signal.SIGINT, self._sigint_handler)

        # load any available persistent application settings
        QtCore.QCoreApplication.setOrganizationName("IDeATe")
        QtCore.QCoreApplication.setOrganizationDomain("ideate.cmu.edu")
        QtCore.QCoreApplication.setApplicationName('mqtt_monitor')
        self.settings = QtCore.QSettings()

        # uncomment to restore 'factory defaults'
        # self.settings.clear()

        # MQTT server settings
        self.hostname = self.settings.value('mqtt_host', 'mqtt.ideate.cmu.edu')
        self.systemHostname = os.popen('uname -n').read().strip()
        self.portnum  = self.settings.value('mqtt_port', 8887)
        self.portnum = int(self.portnum)
        self.username = self.settings.value('mqtt_user', 'students')
        self.password = self.settings.value('mqtt_password', 'Arduino')

        # Create a default subscription based on the username.  The hash mark is a wildcard.
        username = getpass.getuser()
        self.subscription = self.settings.value('mqtt_subscription', username + '/#')

        # default message to send
        self.topic = self.settings.value('mqtt_topic', username)
        self.payload = self.settings.value('mqtt_payload', 'hello')

        # Initialize the MQTT client system
        self.client = mqtt.Client()
        self.client.enable_logger(mqtt_log)
        self.client.on_log = self.on_log
        self.client.on_connect = self.on_connect
        self.client.on_disconnect = self.on_disconnect
        self.client.on_message = self.on_message
        self.client.tls_set()
        return

    ################################################################
    def app_is_exiting(self):
        if self.client.is_connected():
            self.client.disconnect()
            self.client.loop_stop()

    def _sigint_handler(self, signal, frame):
        print("Keyboard interrupt caught, running close handlers...")
        self.app_is_exiting()
        sys.exit(0)

    ################################################################
    def set_server_name(self, name):
        self.hostname = name
        self.settings.setValue('mqtt_host', name)

    def set_server_port(self, value):
        self.portnum = value
        self.settings.setValue('mqtt_port', self.portnum)

    def set_username(self, name):
        self.username = name
        self.settings.setValue('mqtt_user', name)

    def set_password(self, name):
        self.password = name
        self.settings.setValue('mqtt_password', name)

    def connect_to_mqtt_server(self):
        if self.client.is_connected():
            return
        else:
            if self.portnum is None:
                print("Warning: Please specify the server port before attempting connection.")
            else:
                print("Info: Initiating MQTT connection to %s:%d" % (self.hostname, self.portnum))
                self.client.username_pw_set(self.username, self.password)
                self.client.connect_async(self.hostname, self.portnum)
                self.client.loop_start()

    def disconnect_from_mqtt_server(self):
        if self.client.is_connected():
            self.client.disconnect()
        else:
            print("Warning: Not connected.")
        self.client.loop_stop()

    ################################################################
    # The callback for when the broker responds to our connection request.
    def on_connect(self, client, userdata, flags, rc):
        print("Info: Connected to server with with flags: %s, result code: %s" % (flags, rc))

        if rc == 0:
            print("Info: Connection succeeded.")

        elif rc > 0:
            if rc < len(mqtt_rc_codes):
                print("Warning: Connection failed with error: %s", mqtt_rc_codes[rc])
            else:
                print("Warning: Connection failed with unknown error %d", rc)

        # Subscribing in on_connect() means that if we lose the connection and reconnect then subscriptions will be renewed.
        client.subscribe(self.subscription)
        return

    # The callback for when the broker responds with error messages.
    def on_log(client, userdata, level, buf):
        print("Info: on_log level %s: %s", level, userdata)
        return

    def on_disconnect(self, client, userdata, rc):
        print("Info: disconnected")
        return

    # The callback for when a message has been received on a topic to which this
    # client is subscribed.  The message variable is a MQTTMessage that describes
    # all of the message parameters.

    # Some useful MQTTMessage fields: topic, payload, qos, retain, mid, properties.
    #   The payload is a binary string (bytes).
    #   qos is an integer quality of service indicator (0,1, or 2)
    #   mid is an integer message ID.

    def on_message(self, client, userdata, msg):
        global counter
        if msg.topic == "leffen/pc_vitals":
            if counter == 5:
                counter = 0
                msgList = str(msg.payload)
                msgList = msgList.split(",")
                msgList[-1] = msgList[-1][0:2]
                msgList[-1] = msgList[-1].strip()
                if msgList[0][2:] != self.systemHostname:
                    if (float(msgList[1]) < 40.0 and float(msgList[2]) >= 85.0) or \
                        (int(msgList[3]) >= 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) < 40))):
                        # This is the critical case, where something is probably seriously wrong with the system...
                        sys.stdout.write('\a') # Ring terminal bell
                        sys.stdout.flush()
                        self.send_message("leffen/pc_chatter", "Hey %s you good? No? I'm gonna call the Fire Apartment then... WOO-ooo-WOO-ooo!" % msgList[0])
                    elif (float(msgList[1]) >= 40.0 and float(msgList[2]) >= 85.0) or \
                        (int(msgList[3]) >= 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) >= 40))):
                        # This is the case where the system is under load and getting a bit hot...
                        self.send_message("leffen/pc_chatter", "%s looks like you're running a little hot... why not take a break? I think you've earned it." % msgList[0])
                    elif (float(msgList[1]) >= 80.0 and float(msgList[2]) < 85.0) or \
                        (int(msgList[3]) < 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) >= 80 or \
                            int(msgList[4]) == 0))):
                        # This is the case where the system is under load and temps are in-check...
                        self.send_message("leffen/pc_chatter", "Looks like you're on it %s... keep it up!" % msgList[0])
        elif msg.topic == "leffen/human_vitals":
            msgList = str(msg.payload)
            msgList = msgList.split(",")
            if (float(msgList[0]) < 100 or float(msgList[1]) >= 130):
                self.send_message("leffen/to_human", "Hey man, looks like you need a break.")
            if (int(msgList[2]) == 0):
                self.send_message("leffen/to_human", "Alright, if you aren't gonna take care of yourself, then we're going on strike!")
            elif (int(msgList[3]) < 7 or int(msgList[3]) >= 12):
                self.send_message("leffen/to_human", "Come on, man. Get better sleep!")
        counter += 1                
        print("{%s} %s" % (msg.topic, msg.payload))
        return

    ################################################################
    def set_subscription(self, sub):
        if self.client.is_connected():
            self.client.unsubscribe(self.subscription)
            try:
                self.client.subscribe(sub)
                self.subscription = sub
                self.settings.setValue('mqtt_subscription', sub)
            except ValueError:
                print("Error: Invalid subscription string, not changed.")
                self.client.subscribe(self.subscription)
        else:
            self.subscription = sub
            self.settings.setValue('mqtt_subscription', sub)

    def set_topic(self, sub):
        self.topic = sub
        self.settings.setValue('mqtt_topic', sub)

    def send_message(self, topic, payload):
        if self.client.is_connected():
            self.client.publish(topic, payload)
        else:
            print("Error: Not connected.")
        self.payload = payload
        self.settings.setValue('mqtt_payload', payload)

    ################################################################

def main():
    # Optionally add an additional root log handler to stream messages to the
    # terminal console.
    if False:
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.DEBUG)
        console_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
        logging.getLogger().addHandler(console_handler)

    # initialize the Qt system itself
    app = QtWidgets.QApplication(sys.argv)

    # create the main application controller
    main = MainApp()
    main.connect_to_mqtt_server()

    # run the event loop until the user is done
    print("Info: Starting event loop.")
    while True:
        rawCPUTemp = os.popen('sensors').read()
        rawGPU = os.popen('nvidia-smi').read()
        cpuTempLine = rawCPUTemp.splitlines()[5].strip()
        gpuTempLine = rawGPU.splitlines()[9].strip()
        gpuUtilLine = rawGPU.splitlines()[9].strip()
        cpuUtil = psutil.cpu_percent()
        cpuTempstr = cpuTempLine[15:19]
        gpuTempstr = gpuTempLine[8:10]
        cpuUtilstr = str(cpuUtil)
        gpuUtilstr = gpuUtilLine[60:63]
        cpuTemp = float(cpuTempstr)
        gpuTemp = float(gpuTempstr)
        main.send_message("leffen/pc_vitals", main.systemHostname + "," +
            cpuUtilstr + "," + cpuTempstr + "," + gpuTempstr + "," + gpuUtilstr)
        time.sleep(10)
    sys.exit(app.exec_())

################################################################
# Main script follows. This sequence is executed when the script is initiated
# from the command line.

if __name__ == "__main__":
    main()

For Phoenix…

#!/usr/bin/env python3
"""A PyQt5 GUI utility to monitor and send MQTT server messages."""

################################################################
# Written in 2018-2020 by Garth Zeglin <garthz@cmu.edu>

# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.

# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.

################################################################
# standard Python libraries
from __future__ import print_function
import os, sys, struct, time, logging, functools, queue, signal, getpass, psutil

# documentation: https://doc.qt.io/qt-5/index.html
# documentation: https://www.riverbankcomputing.com/static/Docs/PyQt5/index.html
from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork

# documentation: https://www.eclipse.org/paho/clients/python/docs/
import paho.mqtt.client as mqtt

# default logging output
log = logging.getLogger('main')

# logger to pass to the MQTT library
mqtt_log = logging.getLogger('mqtt')
mqtt_log.setLevel(logging.WARNING)

# IDeATE server instances, as per https://mqtt.ideate.cmu.edu/#ports

ideate_ports = { 8884 : '16-223',
                 8885 : '16-375',
                 8886 : '60-223',
                 8887 : '62-362',
}

mqtt_rc_codes = ['Success', 'Incorrect protocol version', 'Invalid client identifier', 'Server unavailable', 'Bad username or password', 'Not authorized']
counter = 0


################################################################
class MainApp(object):
    """Main application object holding any non-GUI related state."""

    def __init__(self):

        # Attach a handler to the keyboard interrupt (control-C).
        signal.signal(signal.SIGINT, self._sigint_handler)

        # load any available persistent application settings
        QtCore.QCoreApplication.setOrganizationName("IDeATe")
        QtCore.QCoreApplication.setOrganizationDomain("ideate.cmu.edu")
        QtCore.QCoreApplication.setApplicationName('mqtt_monitor')
        self.settings = QtCore.QSettings()

        # uncomment to restore 'factory defaults'
        # self.settings.clear()

        # MQTT server settings
        self.hostname = self.settings.value('mqtt_host', 'mqtt.ideate.cmu.edu')
        self.systemHostname = os.popen('uname -n').read().strip()
        self.portnum  = self.settings.value('mqtt_port', 8887)
        self.portnum = int(self.portnum)
        self.username = self.settings.value('mqtt_user', 'students')
        self.password = self.settings.value('mqtt_password', 'Arduino')

        # Create a default subscription based on the username.  The hash mark is a wildcard.
        username = getpass.getuser()
        self.subscription = self.settings.value('mqtt_subscription', username + '/#')

        # default message to send
        self.topic = self.settings.value('mqtt_topic', username)
        self.payload = self.settings.value('mqtt_payload', 'hello')

        # Initialize the MQTT client system
        self.client = mqtt.Client()
        self.client.enable_logger(mqtt_log)
        self.client.on_log = self.on_log
        self.client.on_connect = self.on_connect
        self.client.on_disconnect = self.on_disconnect
        self.client.on_message = self.on_message
        self.client.tls_set()
        return

    ################################################################
    def app_is_exiting(self):
        if self.client.is_connected():
            self.client.disconnect()
            self.client.loop_stop()

    def _sigint_handler(self, signal, frame):
        print("Keyboard interrupt caught, running close handlers...")
        self.app_is_exiting()
        sys.exit(0)

    ################################################################
    def set_server_name(self, name):
        self.hostname = name
        self.settings.setValue('mqtt_host', name)

    def set_server_port(self, value):
        self.portnum = value
        self.settings.setValue('mqtt_port', self.portnum)

    def set_username(self, name):
        self.username = name
        self.settings.setValue('mqtt_user', name)

    def set_password(self, name):
        self.password = name
        self.settings.setValue('mqtt_password', name)

    def connect_to_mqtt_server(self):
        if self.client.is_connected():
            return
        else:
            if self.portnum is None:
                print("Warning: Please specify the server port before attempting connection.")
            else:
                print("Info: Initiating MQTT connection to %s:%d" % (self.hostname, self.portnum))
                self.client.username_pw_set(self.username, self.password)
                self.client.connect_async(self.hostname, self.portnum)
                self.client.loop_start()

    def disconnect_from_mqtt_server(self):
        if self.client.is_connected():
            self.client.disconnect()
        else:
            print("Warning: Not connected.")
        self.client.loop_stop()

    ################################################################
    # The callback for when the broker responds to our connection request.
    def on_connect(self, client, userdata, flags, rc):
        print("Info: Connected to server with with flags: %s, result code: %s" % (flags, rc))

        if rc == 0:
            print("Info: Connection succeeded.")

        elif rc > 0:
            if rc < len(mqtt_rc_codes):
                print("Warning: Connection failed with error: %s", mqtt_rc_codes[rc])
            else:
                print("Warning: Connection failed with unknown error %d", rc)

        # Subscribing in on_connect() means that if we lose the connection and reconnect then subscriptions will be renewed.
        client.subscribe(self.subscription)
        return

    # The callback for when the broker responds with error messages.
    def on_log(client, userdata, level, buf):
        print("Info: on_log level %s: %s", level, userdata)
        return

    def on_disconnect(self, client, userdata, rc):
        print("Info: disconnected")
        return

    # The callback for when a message has been received on a topic to which this
    # client is subscribed.  The message variable is a MQTTMessage that describes
    # all of the message parameters.

    # Some useful MQTTMessage fields: topic, payload, qos, retain, mid, properties.
    #   The payload is a binary string (bytes).
    #   qos is an integer quality of service indicator (0,1, or 2)
    #   mid is an integer message ID.

    def on_message(self, client, userdata, msg):
        global counter
        if msg.topic == "leffen/pc_vitals":
            if counter == 5:
                counter = 0
                msgList = str(msg.payload)
                msgList = msgList.split(",")
                msgList[-1] = msgList[-1][0:2]
                msgList[-1] = msgList[-1].strip()
                if msgList[0][2:] != self.systemHostname:
                    if (float(msgList[1]) < 40.0 and float(msgList[2]) >= 85.0) or \
                        (int(msgList[3]) >= 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) < 40))):
                        # This is the critical case, where something is probably seriously wrong with the system...
                        sys.stdout.write('\a') # Ring terminal bell
                        sys.stdout.flush()
                        self.send_message("leffen/pc_chatter", "Hey %s you good? No? I'm gonna call the Fire Apartment then... WOO-ooo-WOO-ooo!" % msgList[0])
                    elif (float(msgList[1]) >= 40.0 and float(msgList[2]) >= 85.0) or \
                        (int(msgList[3]) >= 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) >= 40))):
                        # This is the case where the system is under load and getting a bit hot...
                        self.send_message("leffen/pc_chatter", "%s looks like you're running a little hot... why not take a break? I think you've earned it." % msgList[0])
                    elif (float(msgList[1]) >= 70.0 and float(msgList[2]) < 85.0) or \
                        (int(msgList[3]) < 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) >= 80))):
                        # This is the case where the system is under load and temps are in-check...
                        self.send_message("leffen/pc_chatter", "Looks like you're on it %s... keep it up!" % msgList[0])
        elif msg.topic == "leffen/human_vitals":
            msgList = str(msg.payload)
            msgList = msgList.split(",")
            if (float(msgList[0]) < 100 or float(msgList[1]) >= 130):
                self.send_message("leffen/to_human", "Hey man, looks like you need a break.")
            if (int(msgList[2]) == 0):
                self.send_message("leffen/to_human", "Alright, if you aren't gonna take care of yourself, then we're going on strike!")
            elif (int(msgList[3]) < 7 or int(msgList[3]) >= 12):
                self.send_message("leffen/to_human", "Come on, man. Get better sleep!")
        counter += 1
        print("{%s} %s" % (msg.topic, msg.payload))
        return

    ################################################################
    def set_subscription(self, sub):
        if self.client.is_connected():
            self.client.unsubscribe(self.subscription)
            try:
                self.client.subscribe(sub)
                self.subscription = sub
                self.settings.setValue('mqtt_subscription', sub)
            except ValueError:
                print("Error: Invalid subscription string, not changed.")
                self.client.subscribe(self.subscription)
        else:
            self.subscription = sub
            self.settings.setValue('mqtt_subscription', sub)

    def set_topic(self, sub):
        self.topic = sub
        self.settings.setValue('mqtt_topic', sub)

    def send_message(self, topic, payload):
        if self.client.is_connected():
            self.client.publish(topic, payload)
        else:
            print("Error: Not connected.")
        self.payload = payload
        self.settings.setValue('mqtt_payload', payload)

    ################################################################

def main():
    # Optionally add an additional root log handler to stream messages to the
    # terminal console.
    if False:
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.DEBUG)
        console_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
        logging.getLogger().addHandler(console_handler)

    # initialize the Qt system itself
    app = QtWidgets.QApplication(sys.argv)

    # create the main application controller
    main = MainApp()
    main.connect_to_mqtt_server()

    # run the event loop until the user is done
    print("Info: Starting event loop.")
    #rawCPU = os.system("sensors"
    while True:
        rawCPUTemp = os.popen('sensors').read()
        rawGPU = os.popen('nvidia-smi').read()
        cpuTempLine = rawCPUTemp.splitlines()[19].strip()
        gpuTempLine = rawGPU.splitlines()[9].strip()
        cpuUtil = psutil.cpu_percent()
        gpuUtilLine = rawGPU.splitlines()[9].strip()
        cpuTempstr = cpuTempLine[16:20]
        gpuTempstr = gpuTempLine[8:10]
        cpuUtilstr = str(cpuUtil)
        gpuUtilstr = gpuUtilLine[60:63]
        cpuTemp = float(cpuTempstr)
        gpuTemp = float(gpuTempstr)
        cpuUtil = float(cpuUtilstr)
        main.send_message("leffen/pc_vitals", main.systemHostname + "," +
            cpuUtilstr + "," + cpuTempstr + "," + gpuTempstr + "," + gpuUtilstr)
        time.sleep(10)
    sys.exit(app.exec_())

################################################################
# Main script follows. This sequence is executed when the script is initiated
# from the command line.

if __name__ == "__main__":
    main()

and for Tars…

#!/usr/bin/env python3
"""A PyQt5 GUI utility to monitor and send MQTT server messages."""

################################################################
# Written in 2018-2020 by Garth Zeglin <garthz@cmu.edu>

# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.

# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.

################################################################
# standard Python libraries
from __future__ import print_function
import os, sys, struct, time, logging, functools, queue, signal, getpass, psutil

# documentation: https://doc.qt.io/qt-5/index.html
# documentation: https://www.riverbankcomputing.com/static/Docs/PyQt5/index.html
from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork

# documentation: https://www.eclipse.org/paho/clients/python/docs/
import paho.mqtt.client as mqtt

# default logging output
log = logging.getLogger('main')

# logger to pass to the MQTT library
mqtt_log = logging.getLogger('mqtt')
mqtt_log.setLevel(logging.WARNING)

# IDeATE server instances, as per https://mqtt.ideate.cmu.edu/#ports

ideate_ports = { 8884 : '16-223',
                 8885 : '16-375',
                 8886 : '60-223',
                 8887 : '62-362',
}

mqtt_rc_codes = ['Success', 'Incorrect protocol version', 'Invalid client identifier', 'Server unavailable', 'Bad username or password', 'Not authorized']
counter = 0


################################################################
class MainApp(object):
    """Main application object holding any non-GUI related state."""

    def __init__(self):

        # Attach a handler to the keyboard interrupt (control-C).
        signal.signal(signal.SIGINT, self._sigint_handler)

        # load any available persistent application settings
        QtCore.QCoreApplication.setOrganizationName("IDeATe")
        QtCore.QCoreApplication.setOrganizationDomain("ideate.cmu.edu")
        QtCore.QCoreApplication.setApplicationName('mqtt_monitor')
        self.settings = QtCore.QSettings()

        # uncomment to restore 'factory defaults'
        # self.settings.clear()

        # MQTT server settings
        self.hostname = self.settings.value('mqtt_host', 'mqtt.ideate.cmu.edu')
        self.systemHostname = os.popen('uname -n').read().strip()
        self.portnum  = self.settings.value('mqtt_port', 8887)
        self.portnum = int(self.portnum)
        self.username = self.settings.value('mqtt_user', 'students')
        self.password = self.settings.value('mqtt_password', 'Arduino')

        # Create a default subscription based on the username.  The hash mark is a wildcard.
        username = getpass.getuser()
        self.subscription = self.settings.value('mqtt_subscription', username + '/#')

        # default message to send
        self.topic = self.settings.value('mqtt_topic', username)
        self.payload = self.settings.value('mqtt_payload', 'hello')

        # Initialize the MQTT client system
        self.client = mqtt.Client()
        self.client.enable_logger(mqtt_log)
        self.client.on_log = self.on_log
        self.client.on_connect = self.on_connect
        self.client.on_disconnect = self.on_disconnect
        self.client.on_message = self.on_message
        self.client.tls_set()
        return

    ################################################################
    def app_is_exiting(self):
        if self.client.is_connected():
            self.client.disconnect()
            self.client.loop_stop()

    def _sigint_handler(self, signal, frame):
        print("Keyboard interrupt caught, running close handlers...")
        self.app_is_exiting()
        sys.exit(0)

    ################################################################
    def set_server_name(self, name):
        self.hostname = name
        self.settings.setValue('mqtt_host', name)

    def set_server_port(self, value):
        self.portnum = value
        self.settings.setValue('mqtt_port', self.portnum)

    def set_username(self, name):
        self.username = name
        self.settings.setValue('mqtt_user', name)

    def set_password(self, name):
        self.password = name
        self.settings.setValue('mqtt_password', name)

    def connect_to_mqtt_server(self):
        if self.client.is_connected():
            return
        else:
            if self.portnum is None:
                print("Warning: Please specify the server port before attempting connection.")
            else:
                print("Info: Initiating MQTT connection to %s:%d" % (self.hostname, self.portnum))
                self.client.username_pw_set(self.username, self.password)
                self.client.connect_async(self.hostname, self.portnum)
                self.client.loop_start()

    def disconnect_from_mqtt_server(self):
        if self.client.is_connected():
            self.client.disconnect()
        else:
            print("Warning: Not connected.")
        self.client.loop_stop()

    ################################################################
    # The callback for when the broker responds to our connection request.
    def on_connect(self, client, userdata, flags, rc):
        print("Info: Connected to server with with flags: %s, result code: %s" % (flags, rc))

        if rc == 0:
            print("Info: Connection succeeded.")

        elif rc > 0:
            if rc < len(mqtt_rc_codes):
                print("Warning: Connection failed with error: %s", mqtt_rc_codes[rc])
            else:
                print("Warning: Connection failed with unknown error %d", rc)

        # Subscribing in on_connect() means that if we lose the connection and reconnect then subscriptions will be renewed.
        client.subscribe(self.subscription)
        return

    # The callback for when the broker responds with error messages.
    def on_log(client, userdata, level, buf):
        print("Info: on_log level %s: %s", level, userdata)
        return

    def on_disconnect(self, client, userdata, rc):
        print("Info: disconnected")
        return

    # The callback for when a message has been received on a topic to which this
    # client is subscribed.  The message variable is a MQTTMessage that describes
    # all of the message parameters.

    # Some useful MQTTMessage fields: topic, payload, qos, retain, mid, properties.
    #   The payload is a binary string (bytes).
    #   qos is an integer quality of service indicator (0,1, or 2)
    #   mid is an integer message ID.

    def on_message(self, client, userdata, msg):
        global counter
        if msg.topic == "leffen/pc_vitals":
            if counter == 6:
                counter = 0
                msgList = str(msg.payload)
                msgList = msgList.split(",")
                msgList[-1] = msgList[-1][0:2]
                msgList[-1] = msgList[-1].strip()
                if msgList[0][2:] != self.systemHostname:
                    if (float(msgList[1]) < 40.0 and float(msgList[2]) >= 85.0) or \
                        (int(msgList[3]) >= 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) < 40))):
                        # This is the critical case, where something is probably seriously wrong with the system...
                        sys.stdout.write('\a') # Ring terminal bell
                        sys.stdout.flush()
                        self.send_message("leffen/pc_chatter", "Hey %s you good? No? I'm gonna call the Fire Apartment then... WOO-ooo-WOO-ooo!" % msgList[0])
                    elif (float(msgList[1]) >= 40.0 and float(msgList[2]) >= 85.0) or \
                        (int(msgList[3]) >= 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) >= 40))):
                        # This is the case where the system is under load and getting a bit hot...
                        self.send_message("leffen/pc_chatter", "%s looks like you're running a little hot... why not take a break? I think you've earned it." % msgList[0])
                    elif (float(msgList[1]) >= 80.0 and float(msgList[2]) < 85.0) or \
                        (int(msgList[3]) < 85 and (len(msgList) < 5 or (msgList[4] != "" and int(msgList[4]) >= 80))):
                        # This is the case where the system is under load and temps are in-check...
                        self.send_message("leffen/pc_chatter", "Looks like you're on it %s... keep it up!" % msgList[0])
        elif msg.topic == "leffen/human_vitals":
            msgList = str(msg.payload)
            msgList = msgList.split(",")
            if (float(msgList[0]) < 100 or float(msgList[1]) >= 130):
                self.send_message("leffen/to_human", "Hey man, looks like you need a break.")
            if (int(msgList[2]) == 0):
                self.send_message("leffen/to_human", "Alright, if you aren't gonna take care of yourself, then we're going on strike!")
            elif (int(msgList[3]) < 7 or int(msgList[3]) >= 12):
                self.send_message("leffen/to_human", "Come on, man. Get better sleep!")
        counter += 1
        print("{%s} %s" % (msg.topic, msg.payload))
        return

    ################################################################
    def set_subscription(self, sub):
        if self.client.is_connected():
            self.client.unsubscribe(self.subscription)
            try:
                self.client.subscribe(sub)
                self.subscription = sub
                self.settings.setValue('mqtt_subscription', sub)
            except ValueError:
                print("Error: Invalid subscription string, not changed.")
                self.client.subscribe(self.subscription)
        else:
            self.subscription = sub
            self.settings.setValue('mqtt_subscription', sub)

    def set_topic(self, sub):
        self.topic = sub
        self.settings.setValue('mqtt_topic', sub)

    def send_message(self, topic, payload):
        if self.client.is_connected():
            self.client.publish(topic, payload)
        else:
            print("Error: Not connected.")
        self.payload = payload
        self.settings.setValue('mqtt_payload', payload)

    ################################################################

def main():
    # Optionally add an additional root log handler to stream messages to the
    # terminal console.
    if False:
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.DEBUG)
        console_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s: %(message)s'))
        logging.getLogger().addHandler(console_handler)

    # initialize the Qt system itself
    app = QtWidgets.QApplication(sys.argv)

    # create the main application controller
    main = MainApp()
    main.connect_to_mqtt_server()

    # run the event loop until the user is done
    print("Info: Starting event loop.")
    while True:
        rawCPUTemp = os.popen('sensors').read()
        rawGPU = os.popen('nvidia-smi').read()
        cpuTempLine = rawCPUTemp.splitlines()[31].strip()
        gpuTempLine = rawGPU.splitlines()[9].strip()
        cpuUtil = psutil.cpu_percent()
        cpuTempstr = cpuTempLine[15:19]
        gpuTempstr = gpuTempLine[8:10]
        cpuUtilstr = str(cpuUtil)
        cpuTemp = float(cpuTempstr)
        gpuTemp = float(gpuTempstr)
        main.send_message("leffen/pc_vitals", main.systemHostname + "," +
            cpuUtilstr + "," + cpuTempstr + "," + gpuTempstr)
        time.sleep(10)
    sys.exit(app.exec_())

################################################################
# Main script follows. This sequence is executed when the script is initiated
# from the command line.

if __name__ == "__main__":
    main()

Here’s the code that runs on the Arduino. It’s nothing fancy–it’s just sending the relevant data as a comma-separated string over serial, which is then picked up by the computer that the Arduino is connected to.

/*
   Human Gateway (62-362 Project 3)
   Seth Geiser (sgeiser)

   Collaboration: Same as Project 2

   GSR Sensor: https://wiki.seeedstudio.com/Grove-GSR_Sensor/
   MAX30102 PulseOx: Example 5: Heart-rate
   Keypad: Same as Project 1, Workaholic's Clock
   DFPlayer Mini: Same as Project 1, Workaholic's Clock

   This code initializes a keypad, 20x4 I2C LCD, Pulse
   Oximeter, Grove GSR sensor, and a DFRobot DFPlayer
   Mini. The keypad is for entering when I went to sleep an
   how long I slept. The GSR sensor measures galvanic skin
   response, similar to a polygraph test. The Pulse Oximeter
   is currently only being used as a heart-rate monitor,
   since the SPO2 functionality appears to not be
   working right now (tested with the SPO2 example
   code).

   Pin mapping:

   pin      | mode       | description
   ---------|------------|------------
   SDA/SCL   I2C          MAX30102 PulseOx + 20x4 I2C LCD
   2-8       Drive/Sense  Keypad
   9         RX           DFPlayer Mini TX
   10        TX           DFPlayer Mini RX
   A0        INPUT        GSR Sensor (through Grove shield)
*/

#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <MAX30105.h>
#include <heartRate.h>

SoftwareSerial mySoftwareSerial(9, 10); // RX, TX
DFRobotDFPlayerMini myDFPlayer;

MAX30105 particleSensor;

LiquidCrystal_I2C lcd(0x27, 20, 4);
// set the LCD address to 0x27 for 20x4 display

char customKey = 0;
const byte ROWS = 4;
const byte COLS = 3;
char hexaKeys[ROWS][COLS] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};

byte rowPins[ROWS] = {5, 3, 2, 7};
byte colPins[COLS] = {4, 8, 6};
Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);

const byte RATE_SIZE = 4; // Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE]; // Array of heart rates
byte rateSpot = 0;
long lastBeat = 0; // Time at which the last beat occurred

float beatsPerMinute;
int beatAvg;

const int GSR = A0;
int sensorValue = 0;
int gsr_average = 0;

int j = 0;
int sleepStart;
int sleepAmount;

// For accepting input from the keypad, these get converted
// to the ints above...
char sleep[2];

void(* resetFunc) (void) = 0; // reset the Arduino when done
void setup() {
  lcd.init();
  lcd.backlight();
  mySoftwareSerial.begin(9600);
  Serial.begin(115200);
  Serial.println("Human,Arduino UNO R3 SMD Connected...");

  lcd.setCursor(0, 0);
  lcd.print(F("Arduino UNO R3 SMD"));
  lcd.setCursor(0, 1);
  lcd.print(F("Starting DFPlayer..."));
  lcd.setCursor(0, 2);
  lcd.print(F("Starting MAX30102..."));
  lcd.setCursor(0, 3);
  lcd.print(F("Please wait..."));
  delay(2000);
  // Initialize sensor
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST))
    // Use default I2C port, 400kHz speed
  {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(F("MAX30102 not found."));
    lcd.setCursor(0, 1);
    lcd.print(F("Please check wiring."));
    while (1);
  }

  particleSensor.setup();
  // Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A);
  // Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeGreen(0);
  // Turn off Green LED

  if (!myDFPlayer.begin(mySoftwareSerial)) {
    // Use softwareSerial to communicate with mp3.
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(F("Unable to begin:"));
    lcd.setCursor(0, 1);
    lcd.print(F("Please check wiring!"));
    lcd.setCursor(0, 2);
    lcd.print(F("Please insert SD!"));
  }
  lcd.setCursor(0, 3);
  lcd.print(F("DFPlayerMini online."));
  myDFPlayer.volume(20);  // Set volume value. From 0 to 30
  delay(2000);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("CMU IDeATe PhysComp"));
  lcd.setCursor(0, 1);
  lcd.print(F("Human Gateway"));
  lcd.setCursor(0, 2);
  lcd.print(F("Powered by sgeiser"));
  lcd.setCursor(0, 3);
  lcd.print(F("BOOTING DEVICE..."));
  delay(2500);
  myDFPlayer.play(1); // 0001_ps2.mp3
  delay(2500);
  lcd.clear();
  customKey = customKeypad.getKey();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Input when you went"));
  lcd.setCursor(0, 1);
  lcd.print(F("to sleep and how"));
  lcd.setCursor(0, 2);
  lcd.print(F("long you slept in"));
  lcd.setCursor(0, 3);
  lcd.print(F("24-hour time."));
  delay(5000);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("If any value is less"));
  lcd.setCursor(0, 1);
  lcd.print(F("than 10, then prefix"));
  lcd.setCursor(0, 2);
  lcd.print(F("with a zero."));
  delay(5000);
  lcd.clear();
  inputSleepPattern();
}

void loop() {
  customKey = customKeypad.getKey();
  long sum = 0;
  for (int i = 0; i < 10; i++) {
    sensorValue = analogRead(GSR);
    sum += sensorValue;
  }
  gsr_average = sum / 10;
  long irValue = particleSensor.getIR();

  if (checkForBeat(irValue) == true)
  {
    // We sensed a beat!
    long delta = millis() - lastBeat;
    lastBeat = millis();

    beatsPerMinute = 60 / (delta / 1000.0);

    if (beatsPerMinute < 255 && beatsPerMinute > 20)
    {
      rates[rateSpot++] = (byte)beatsPerMinute;
      // Store this reading in the array
      rateSpot %= RATE_SIZE; // Wrap variable

      // Take average of readings
      beatAvg = 0;
      for (byte x = 0 ; x < RATE_SIZE ; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }
  lcd.setCursor(0, 0);
  lcd.print((String)"Average GSR: " + gsr_average + "    ");
  lcd.setCursor(0, 1);
  lcd.print((String)"Average BPM: " + beatAvg + "    ");
  lcd.setCursor(0, 2);
  lcd.print(F("#: Assess vitals"));
  if (customKey == '#') {
    lcd.clear();
    assess();
  }
}

void inputSleepStart() {
  lcd.setCursor(0, 0);
  lcd.print(F("Sleep start: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      sleep[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  sleepStart = atoi(sleep);
  // Convert the char array to int and store it. Note
  // that atoi is deprecated, am going to change it soon.
  j = 0;
}

void inputSleepAmount() {
  lcd.setCursor(0, 1);
  lcd.print(F("Sleep amount: "));
  while (j < 2) {
    char key = customKeypad.getKey();
    if (key) {
      sleep[j++] = key;
      lcd.print(key);
    }
    key = 0;
  }
  sleepAmount = atoi(sleep);
  // Convert the char array to int and store it. Note
  // that atoi is deprecated, am going to change it soon.
  j = 0;
}

// I am not fond of how I wrote these two functions.
// I hope to condense them.

void inputSleepPattern() {
  inputSleepStart();
  inputSleepAmount();
  lcd.clear();
}

void assess() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.setCursor(0, 2);
  lcd.print(F("Checking your vitals"));
  delay(2500);
  if (gsr_average > 500) {
    lcd.setCursor(0, 3);
    lcd.print(F("Could not read GSR."));
    delay(2500);
  }
  if (beatAvg <= 40) {
    lcd.setCursor(0, 3);
    lcd.print(F("Could not get pulse."));
    delay(2500);
  }
  if ((gsr_average < 150 || beatAvg >= 130) && (sleepStart <= 6 || (sleepAmount < 7 || sleepAmount > 12))) {
    allBad();
  }
  else if (gsr_average < 150 || beatAvg >= 130) {
    stressed();
  }
  else if (sleepStart <= 6 || (sleepAmount < 7 || sleepAmount > 12)) {
    badSleep();
  }
  else {
    allGood();
  }
  Serial.println(String(gsr_average) + "," + String(beatAvg) + "," + String(sleepStart) + "," + String(sleepAmount));
  resetFunc();
}

void badSleep() {
  lcd.setCursor(0, 3);
  lcd.print(F("Get better sleep!   "));
  delay(5000);
}

void stressed() {
  lcd.setCursor(0, 3);
  lcd.print(F("You're stressed..."));
  delay(2500);
  lcd.setCursor(0, 3);
  lcd.print(F("Take a break, dude."));
  delay(5000);
}

void allBad() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Oh my God, it's all"));
  lcd.setCursor(0, 1);
  lcd.print(F("broken! Seriously,"));
  lcd.setCursor(0, 2);
  lcd.print(F("you need to get on"));
  lcd.setCursor(0, 3);
  lcd.print(F("top of this..."));
  myDFPlayer.play(5); // 0005_battery_low.mp3
  delay(5000);
}

void allGood() {
  lcd.setCursor(0, 3);
  lcd.print(F("Your vitals are OK!"));
  delay(2500);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(F("Nicely done! Your"));
  lcd.setCursor(0, 1);
  lcd.print(F("vitals look alright,"));
  lcd.setCursor(0, 2);
  lcd.print(F("and your systems are"));
  lcd.setCursor(0, 3);
  lcd.print(F("running smoothly!"));
  myDFPlayer.play(3); // 0003_scatman_world.mp3
  delay(13000);
}