Skip to content
Snippets Groups Projects
smartwatch.py 7.79 KiB
Newer Older
#Reference for Threading and GUI architecture: https://maldus512.medium.com/how-to-setup-correctly-an-application-with-python-and-tkinter-107c6bc5a45
#Reference for function animate https://towardsdatascience.com/plotting-live-data-with-matplotlib-d871fac7500b
import tkinter as tk
import matplotlib.pyplot as plt
import numpy as np
from tkinter import TOP, BOTH, X, LEFT, RIGHT
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.animation import FuncAnimation
from types import SimpleNamespace
from enum import Enum
from threading import Thread
from hardware_interface import Hardware_interface

TIME_OUT = 0.5  #Time out used for refreshing the User Interface
PPG_QUEUE_MAX_LENGTH = 1000 #1000 points will be displayed per PPG (10mS/point x 1000 points = 10 seconds )
CHART_MARGIN_PERCENTAGE = 1.1     #Increase Y Lims of the chart to improve the visualization. 10% in this case.

class Messages(Enum):
    HRSETALARM = 0

#Method in charge of updating the UI. It is called from another thread and uses queue for communication
def refreshUI(guiRef, model, guiQueue, ppgQueue):
    msg = None
    while True:
        try:
            msg = guiQueue.get(timeout = TIME_OUT)

            if msg == Messages.HRSETALARM:
                model.count += 1
                guiRef.entry["state"] = 'normal'
                guiRef.entry.delete(0,"end")
                guiRef.entry.insert(0,"Valor")
                guiRef.entry["state"] = 'disabled'
                device.set_alarm(Hardware_interface.MESSAGE_ID_HR_ALARM, 80, 100)

        except queue.Empty:
            pass 

        #Other tasks...
        #Getting data from device object
        dataAvailable = device.new_data_available()

        if(dataAvailable == True):
            #Process message
            #If message adheres to protocol, update queues or instant values
            guiRef.entry_two.delete(0,"end")
            guiRef.entry_two.insert(0, "Dato nuevo")
            #TODO Do Shallow copies of the queues
            #guiRef.ppgQueue.put()

#Class for the User Interface
class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        #self.master.geometry('1000x1000')
        self.master.title('Smart Watch')
        self.pack(fill=BOTH, expand=True)
        
        #Animate needs to be created here and declared self. If not, will be gargabe collected
        self.ppgQueue = queue.Queue()
        self.chartQueue = collections.deque(np.zeros(PPG_QUEUE_MAX_LENGTH), maxlen=PPG_QUEUE_MAX_LENGTH)
        self.figure, self.ax = plt.subplots(1, 1, figsize=(6,5))
        self.create_widgets()
        self.animate = FuncAnimation(self.figure, self.refreshChart, interval=500)

    def create_widgets(self):
    
    #FRAME 1 
        self.frame_one = tk.Frame(self.master)
        self.frame_one.pack(fill=X)
        self.label_title = tk.Label(self.frame_one, text = "Smart Watch")
        self.label_title.pack(side=LEFT)
    #FRAME 2 - GRAPH
        self.frame_two = tk.Frame(self.master)
        self.frame_two.pack(fill=X)
        chart_type = FigureCanvasTkAgg(self.figure, self.frame_two)
        chart_type.get_tk_widget().pack()

    #FRAME 3
        self.frame_three = tk.Frame(self.master)
        self.frame_three.pack(fill=X)

        self.label_subtitle_two = tk.Label(self.frame_three, text = "Valores Instantáneos")
        self.label_subtitle_two.pack(side=LEFT)

    #FRAME 4
        self.frame_four = tk.Frame(self.master)
        self.frame_four.pack(fill=X)
        
        self.label_heartrate = tk.Label(self.frame_four, text = "Heart Rate")
        self.label_heartrate.pack(side=LEFT)

        #TODO Entry must be enabled before updating it

        self.entry_heartrate = tk.Entry(self.frame_four, state='disabled')
        self.entry_heartrate.pack(side=LEFT)

        self.label_heartrate_units_one = tk.Label(self.frame_four, text = "bpm")
        self.label_heartrate_units_one.pack(side=LEFT)

        self.label_heartrate_alarm = tk.Label(self.frame_four, text = "ALARMA")
        self.label_heartrate_alarm.pack(side=LEFT)

        self.label_heartrate_minimum = tk.Label(self.frame_four, text = "Mínimo")
        self.label_heartrate_minimum.pack(side=LEFT)

        self.entry_heartrate_minimum = tk.Entry(self.frame_four)
        self.entry_heartrate_minimum.pack(side=LEFT)

        self.label_heartrate_units_two = tk.Label(self.frame_four, text = "bpm")
        self.label_heartrate_units_two.pack(side=LEFT)

        self.label_heartrate_maximum = tk.Label(self.frame_four, text = "Máximo")
        self.label_heartrate_maximum.pack(side=LEFT)

        self.entry_heartrate_maximum = tk.Entry(self.frame_four)
        self.entry_heartrate_maximum.pack(side=LEFT)

        self.label_units_heartrate_three = tk.Label(self.frame_four, text = "bpm")
        self.label_units_heartrate_three.pack(side=LEFT)

        self.button_heartrate = tk.Button(self.frame_four, text = "Configurar alarma")
        self.button_heartrate.pack(side=LEFT)

    #FRAME 5
        self.frame_five = tk.Frame(self.master)
        self.frame_five.pack(fill=X)

        self.quit = tk.Button(self.frame_five, text="QUIT", fg="red", command=self.master.destroy)
        self.quit.pack(side=RIGHT)

    #Method that provides reference to the user interface. It uses a queue to communicate the UI with the exterior. It returns references to 
    #inner objetcs so the exterior can communicate with it. More references can be given in "SimpleNameSpace"
    def gui_reference(self, root, queue, ppgQueue):
        entry = self.entry_heartrate
        entry["state"] = 'normal'
        entry.delete(0,"end")
        entry.insert(0,"10")
        entry["state"] = 'disabled'

        self.button_heartrate["command"] = lambda : queue.put(Messages.HRSETALARM)
        
        #Just for testing purposes to emit reference to another widget
        entry_two = self.entry_heartrate_minimum
        return SimpleNamespace(entry=entry, entry_two=entry_two, ppgQueue=ppgQueue)
    #Function used to update the chart with external data. It is called every 500 mseconds. Data needs to be passed by reference using "gui_reference"
    def refreshChart(self, x):
        #get data
        while(self.ppgQueue.empty() == False):
            self.chartQueue.popleft()
            #TODO Verify with index that there is no missing value. Add None if there are any...
            self.chartQueue.append(self.ppgQueue.get())
        #Get limits to update chart
        copyOfChartQueue = self.chartQueue.copy()
        maxValue = 0
        minValue = 0
        for p in copyOfChartQueue:
            if(type(p) == int):
                if p > maxValue:
                    maxValue = p
                if p < minValue:
                    minValue = p


        self.ax.cla()
        self.ax.plot(self.chartQueue, color='tab:blue')
        #print("Chart refreshed")
        self.ax.set_xlim([0, PPG_QUEUE_MAX_LENGTH])
        self.ax.set_ylim([minValue * CHART_MARGIN_PERCENTAGE, maxValue * CHART_MARGIN_PERCENTAGE])
        self.ax.set_title('PPG - RED')

if __name__ == '__main__':

    root = tk.Tk()
    app = Application(master=root)

    #Queue used to communicate the UI with the exterior
    guiQueue = queue.Queue()
    ppgQueue = queue.Queue(maxsize=PPG_QUEUE_MAX_LENGTH)

    guiRef = app.gui_reference(root, guiQueue, ppgQueue)
    model = SimpleNamespace(count=0)

    #Object for communication with MSP432 and for processing values
    device = Hardware_interface()
    
    #Thread used to separate the use interface with the front end. It is used to not slow down the GUI when processing information
    t = Thread(target=refreshUI, args=(guiRef, model, guiQueue, ppgQueue, ))
    app.mainloop()