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 matplotlib.pyplot as plt
import numpy as np
import collections
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["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()
#Process message
#If message adheres to protocol, update queues or instant values
guiRef.entry_two.insert(0, "Dato nuevo")
#TODO Do Shallow copies of the queues
#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.animate = FuncAnimation(self.figure, self.refreshChart, interval=500)
#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)
self.frame_two = tk.Frame(self.master)
self.frame_two.pack(fill=X)
chart_type = FigureCanvasTkAgg(self.figure, self.frame_two)
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#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)
ppgQueue = self.ppgQueue
#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')
#set limits and title
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()
#Queue used to store PPG data
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, ))
t.daemon = True
t.start()