11

INTERFACE AND APPLICATION PROGRAMMING

This week, I learned using tkinter to make a GUI for my input device. I made a step response sensor, and the Neil's example shows a bar plot of that time. Therefore, I decide to make GUI that can track the trajectory of the value. Following this tutorial, I made a GUI that use matplotlib to visualize the signal. The process was pretty smooth, I only met a small problem that the visualized curve was delayed. I adjusted the time interval between animation updates and it was fixed.

Tools: Python, Tkinter.
Date: 11.23.2022 - 11.29.2022






PROCESS

THIS IS HOW I GOT THERE

Neil's GUI.

My GUI.

The code for the GUI is shown below.

import datetime as dt
import tkinter as tk
import tkinter.font as tkFont
import serial,sys

import matplotlib.figure as figure
import matplotlib.animation as animation
import matplotlib.dates as mdates
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

###############################################################################
# Parameters and global variables

# Parameters
update_interval = 5 # Time (ms) between polling/animation updates
max_elements = 500    # Maximum number of elements to store in plot lists

# Declare global variables
root = None
dfont = None
frame = None
canvas = None
ax1 = None
temp_plot_visible = None


# Global variable to remember various states
fullscreen = False
temp_plot_visible = True
light_plot_visible = True

if (len(sys.argv) != 2):
    print("command line: my_txrx.py serial_port")
    sys.exit()
port = sys.argv[1]
#
# open serial port
#
ser = serial.Serial(port,115200)
ser.setDTR()

###############################################################################
# Functions

# Toggle fullscreen
def toggle_fullscreen(event=None):

    global root
    global fullscreen

    # Toggle between fullscreen and windowed modes
    fullscreen = not fullscreen
    root.attributes('-fullscreen', fullscreen)
    resize(None)   

# Return to windowed mode
def end_fullscreen(event=None):

    global root
    global fullscreen

    # Turn off fullscreen mode
    fullscreen = False
    root.attributes('-fullscreen', False)
    resize(None)

# Automatically resize font size based on window size
def resize(event=None):

    global dfont
    global frame

    # Resize font based on frame height (minimum size of 12)
    # Use negative number for "pixels" instead of "points"
    new_size = -max(12, int((frame.winfo_height() / 15)))
    dfont.configure(size=new_size)

# Toggle the temperature plot
def toggle_temp():

    global canvas
    global ax1
    global temp_plot_visible

    # Toggle plot and axis ticks/label
    temp_plot_visible = not temp_plot_visible
    ax1.collections[0].set_visible(temp_plot_visible)
    ax1.get_yaxis().set_visible(temp_plot_visible)
    canvas.draw()

def toggle_value():

    global canvas
    global ax1
    global temp_plot_visible

    # Toggle plot and axis ticks/label
    temp_plot_visible = not temp_plot_visible
    ax1.collections[0].set_visible(temp_plot_visible)
    ax1.get_yaxis().set_visible(temp_plot_visible)
    canvas.draw()

# Toggle the light plot
def toggle_light():

    global canvas
    global ax2
    global light_plot_visible

    # Toggle plot and axis ticks/label
    light_plot_visible = not light_plot_visible
    ax2.get_lines()[0].set_visible(light_plot_visible)
    ax2.get_yaxis().set_visible(light_plot_visible)
    canvas.draw()

# This function is called periodically from FuncAnimation
def animate(i, ax1, xs, values, value_):

    # Update data to display temperature and light values
    try:
        new_value = int(ser.readline().rstrip().decode())
    except:
        new_value = 0
        pass

    # Update our labels
    value_.set(new_value)

    # Append timestamp to x-axis list
    timestamp = mdates.date2num(dt.datetime.now())
    xs.append(timestamp)

    # Append sensor data to lists for plotting
    values.append(new_value)

    # Limit lists to a set number of elements
    xs = xs[-max_elements:]
    values = values[-max_elements:]

    # Clear, format, and plot light values first (behind)
    color = 'tab:red'
    ax1.clear()
    ax1.set_ylabel('Rx-Tx', color=color)
    ax1.tick_params(axis='y', labelcolor=color)
    ax1.fill_between(xs, values, 0, linewidth=2, color=color, alpha=0.3)

    # Format timestamps to be more readable
    ax1.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
    fig.autofmt_xdate()

    # Make sure plots stay visible or invisible as desired
    ax1.collections[0].set_visible(temp_plot_visible)

# Dummy function prevents segfault
def _destroy(event):
    pass

###############################################################################
# Main script

# Create the main window
root = tk.Tk()
root.title("Sensor Dashboard")

# Create the main container
frame = tk.Frame(root)
frame.configure(bg='white')

# Lay out the main container (expand to fit window)
frame.pack(fill=tk.BOTH, expand=1)

# Create figure for plotting
fig = figure.Figure(figsize=(2, 2))
fig.subplots_adjust(left=0.1, right=0.8)
ax1 = fig.add_subplot(1, 1, 1)

# Empty x and y lists for storing data to plot later
xs = []
values = []

# Variables for holding temperature and light data
value_ = tk.DoubleVar()

# Create dynamic font for text
dfont = tkFont.Font(size=-24)

# Create a Tk Canvas widget out of our figure
canvas = FigureCanvasTkAgg(fig, master=frame)
canvas_plot = canvas.get_tk_widget()

# Create other supporting widgets
label_temp = tk.Label(frame, text='High-Low:    ', font=dfont, bg='white')
label_celsius = tk.Label(frame, textvariable=value_, font=dfont, bg='white')
button_temp = tk.Button(    frame, 
                            text="Toggle", 
                            font=dfont,
                            command=toggle_value)
button_quit = tk.Button(    frame,
                            text="Quit",
                            font=dfont,
                            command=root.destroy)

# Lay out widgets in a grid in the frame
canvas_plot.grid(   row=0, 
                    column=0, 
                    rowspan=5, 
                    columnspan=4, 
                    sticky=tk.W+tk.E+tk.N+tk.S)
label_temp.grid(row=0, column=4, columnspan=2)
label_celsius.grid(row=1, column=4, sticky=tk.E)
button_temp.grid(row=5, column=0, columnspan=2)
button_quit.grid(row=5, column=4, columnspan=2)

# Add a standard 5 pixel padding to all widgets
for w in frame.winfo_children():
    w.grid(padx=5, pady=5)

# Make it so that the grid cells expand out to fill window
for i in range(0, 5):
    frame.rowconfigure(i, weight=1)
for i in range(0, 5):
    frame.columnconfigure(i, weight=1)

# Bind F11 to toggle fullscreen and ESC to end fullscreen
root.bind('', toggle_fullscreen)
root.bind('', end_fullscreen)

# Have the resize() function be called every time the window is resized
root.bind('', resize)

# Call empty _destroy function on exit to prevent segmentation fault
root.bind("", _destroy)

# Call animate() function periodically
fargs = (ax1, xs, values, value_)
ani = animation.FuncAnimation(  fig, 
                                animate, 
                                fargs=fargs, 
                                interval=update_interval)               

# Start in fullscreen mode and run
# toggle_fullscreen()
root.mainloop()