# Python GUI Guide: Introduction to Tkinter

Pages
Contributors: Shawn Hymel

## Experiment 3: Sensor Dashboard

In this next experiment, we're going to connect a couple of I2C sensors and display their information in real time on our monitor. We start by just showing the sensor's numerical values and then bring in Matplotlib to create a live updating graph of that data. Note that these are just example sensors; feel free to use whatever sensors you'd like for your particular application.

### Hardware Connections

Connect a TMP102 Temperature Sensor breakout and APDS-9301 Ambient Light Sensor breakout to the Raspberry Pi as shown in the diagrams below.

Connecting through a Pi Wedge:

Connecting directly to the Raspberry Pi:

### Sensor Modules

To simplify our I2C reading and writing, we're going to copy in Python modules to read data from the TMP102 and APDS-9301 sensors. Open a new file named tmp102.py:

``````language:bash
nano tmp102.py
``````

Copy in the following Python code:

``````language:python
import smbus

# Module variables
i2c_ch = 1
bus = None

# TMP102 address on the I2C bus

reg_temp = 0x00
reg_config = 0x01

# Calculate the 2's complement of a number
def twos_comp(val, bits):
if (val & (1 << (bits - 1))) != 0:
val = val - (1 << bits)
return val

# Read temperature registers and calculate Celsius

global bus

temp_c = (val[0] << 4) | (val[1] >> 4)

# Convert to 2s complement (temperatures can be negative)
temp_c = twos_comp(temp_c, 12)

# Convert registers value to temperature (C)
temp_c = temp_c * 0.0625

return temp_c

# Initialize communications with the TMP102
def init():

global bus

# Initialize I2C (SMBus)
bus = smbus.SMBus(i2c_ch)

# Read the CONFIG register (2 bytes)

# Set to 4 Hz sampling (CR1, CR0 = 0b10)
val[1] = val[1] & 0b00111111
val[1] = val[1] | (0b10 << 6)

# Write 4 Hz sampling back to CONFIG

# Read CONFIG to verify that we changed it
``````

Save the code with ctrl + x, press y, and press enter. This module allows us to call `init()` and `read_temp()` functions to initialize and read temperature data from the TMP102.

Similarly, we need to create a module for our APDS-9301. Create a new file named apds9301.py:

``````language:bash
nano apds9301.py
``````

Copy in the following code:

``````language:python
import smbus

# Module variables
i2c_ch = 1
bus = None

# APDS-9301 address on the I2C bus

apds9301_control_reg = 0x80
apds9301_timing_reg = 0x81
apds9301_data0low_reg = 0x8C
apds9301_data1low_reg = 0x8E

# Initialize communications and turn on the APDS-9301
def init():

global bus

# Initialize I2C (SMBus)
bus = smbus.SMBus(i2c_ch)

# Read the CONTROL register (1 byte)

# Set POWER to on in the CONTROL register
val[0] = val[0] & 0b11111100
val[0] = val[0] | 0b11

# Enable the APDS-9301 by writing back to CONTROL register

# Read light data from sensor and calculate lux

global bus

# Read channel 0 light value and combine 2 bytes into 1 number
ch0 = (val[1] << 8) | val[0]

# Read channel 1 light value and combine 2 bytes into 1 number
ch1 = (val[1] << 8) | val[0]

# Make sure we don't divide by 0
if ch0 == 0.0:
return 0.0

# Calculate ratio of ch1 and ch0
ratio = ch1 / ch0

# Assume we are using the default 13.7 ms integration time on the sensor
# So, scale raw light values by 1/0.034 as per the datasheet
ch0 *= 1 / 0.034
ch1 *= 1 / 0.034

# Assume we are using the default low gain setting
# So, scale raw light values by 16 as per the datasheet
ch0 *= 16;
ch1 *= 16;

# Calculate lux based on the ratio as per the datasheet
if ratio <= 0.5:
return (0.0304 * ch0) - ((0.062 * ch0) * ((ch1/ch0) ** 1.4))
elif ratio <= 0.61:
return (0.0224 * ch0) - (0.031 * ch1)
elif ratio <= 0.8:
return (0.0128 * ch0) - (0.0153 * ch1)
elif ratio <= 1.3:
return (0.00146 * ch0) - (0.00112*ch1)
else:
return 0.0
``````

Save and exit with ctrl + x, y, and enter. Like our tmp102 module, we can call `init()` and `read_lux()` to initialize and read the ambient light values from the APDS-9301 sensor.

Note: Make sure that tmp102.py and apds9301.py are in the same directory as your main application code. Otherwise, your import statements will not be able to find your modules.

### Code Part 1: Fullscreen Numerical Dashboard

Let's start by making a simple display that takes up the full screen and shows the numerical temperature and ambient light values. Copy the following code into a new file:

``````language:python
import tkinter as tk
import tkinter.font as tkFont

import tmp102
import apds9301

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

# Declare global variables
root = None
dfont = None
frame = None
temp_c = None
lux = None

# Global variable to remember if we are fullscreen or windowed
fullscreen = False

###############################################################################
# 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()

def end_fullscreen(event=None):

global root
global fullscreen

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

# 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() / 10)))
dfont.configure(size=new_size)

# Read values from the sensors at regular intervals
def poll():

global root
global temp_c
global lux

# Update labels to display temperature and light values
try:
temp_c.set(val)
lux.set(val)
except:
pass

# Schedule the poll() function for another 500 ms from now
root.after(500, poll)

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

# Create the main window
root = tk.Tk()
root.title("The Big Screen")

# Create the main container
frame = tk.Frame(root)

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

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

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

# Create widgets
label_temp = tk.Label(frame, text="Temperature:", font=dfont)
label_celsius = tk.Label(frame, textvariable=temp_c, font=dfont)
label_unitc = tk.Label(frame, text="°C", font=dfont)
label_light = tk.Label(frame, text="Light:", font=dfont)
label_lux = tk.Label(frame, textvariable=lux, font=dfont)
label_unitlux = tk.Label(frame, text="lux", font=dfont)
button_quit = tk.Button(frame, text="Quit", font=dfont, command=root.destroy)

# Lay out widgets in a grid in the frame

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

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

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

# Initialize our sensors
tmp102.init()
apds9301.init()

# Schedule the poll() function to be called periodically
root.after(500, poll)

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

Save the file with a name like tkinter_fullscreen.py and run it. Your entire screen should be taken over by the GUI, and you should see the local ambient temperature and light values displayed. Try covering the light sensor or breathing on the temperature sensor to change their values. Press esc to exit fullscreen or press F11 to toggle fullscreen on and off.

Code to Note:

To control having our application take up the entire screen, we use the following method:

``````language:python
root.attributes('-fullscreen', fullscreen)
``````

where the `fullscreen` variable is a boolean (True or False). If you look toward the end of the code, you'll see the following two lines:

``````language:python
root.bind('<F11>', toggle_fullscreen)
root.bind('<Escape>', end_fullscreen)
``````

These bind the key presses F11 and esc to the toggle_fullscreen() and end_fullscreen() functions, respectively. These allow the user to control if the application takes up the entire screen or is in a window.

We also use the `rowconfigure()` and `columnconfigure()` methods again to control how the grid cells resize within the window. We combine this with a dynamic font:

``````language:python
dfont = tkFont.Font(size=-24)
``````

Note that the negative number (-24) means we want to specify the font size in pixels instead of "points." We also have our `resize()` function called every time the window is resized with the following:

``````language:python
root.bind('<Configure>', resize)
``````

In our `resize()` function, we calculate a new font size based on the height of the resized frame with:

``````language:python
new_size = -max(12, int((frame.winfo_height() / 10)))
``````

This says that the new font size should be the height of the frame divided by 10, but no smaller than 12. We turn it into a negative value, as we want to specify font height in pixels instead of points (once again). We then set the new font size with:

``````language:python
dfont.configure(size=new_size)
``````

Try it! With the application running, press esc to exit fullscreen mode and try resizing the window. You should see the text grow and shrink as necessary. It's not perfect, as certain aspect ratios will cut off portions of the text, but it should work without a problem in fullscreen mode (the intended application).

If you are using a touchscreen, you might not have an easy way for users to resize the window or quit out of the application (in some instances, that might be a good thing, but for our example, we want users to be able to exit). To accomplish this, we add a "Quit" button to our GUI:

``````language:python
button_quit = tk.Button(frame, text="Quit", font=dfont, command=root.destroy)
``````

We assign the callback function to be `root.destroy`. This is a built-in method within Tkinter that says to close the associated window and exit out of `mainloop`.

You'll also notice that we are relying on the `after()` method again to call our `poll()` function at regular intervals.

### Code Part 2: Complete Dashboard with Plotting

Now it's time to get fancy. Let's take the basic dashboard concept and add plotting. To do this, we'll need to pull in the Matplotlib package. If you have not already installed it, run the following commands in a terminal:

``````language:bash
sudo apt-get update
sudo apt-get install libatlas3-base libffi-dev at-spi2-core python3-gi-cairo
pip install cairocffi
pip install matplotlib
``````

In a new file, copy in the following code:

``````language:python
import datetime as dt
import tkinter as tk
import tkinter.font as tkFont

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

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

# Parameters
update_interval = 60000 # Time (ms) between polling/animation updates
max_elements = 1440     # 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

###############################################################################
# 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)

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()

# 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, ax2, xs, temps, lights, temp_c, lux):

# Update data to display temperature and light values
try:
except:
pass

# Update our labels
temp_c.set(new_temp)
lux.set(new_lux)

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

# Append sensor data to lists for plotting
temps.append(new_temp)
lights.append(new_lux)

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

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

# Clear, format, and plot temperature values (in front)
color = 'tab:blue'
ax2.clear()
ax2.set_ylabel('Light (lux)', color=color)
ax2.tick_params(axis='y', labelcolor=color)
ax2.plot(xs, lights, linewidth=2, color=color)

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

# Make sure plots stay visible or invisible as desired
ax1.collections[0].set_visible(temp_plot_visible)
ax2.get_lines()[0].set_visible(light_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))

# Instantiate a new set of axes that shares the same x-axis
ax2 = ax1.twinx()

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

# Variables for holding temperature and light data
temp_c = tk.DoubleVar()
lux = 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='Temperature:', font=dfont, bg='white')
label_celsius = tk.Label(frame, textvariable=temp_c, font=dfont, bg='white')
label_unitc = tk.Label(frame, text="C", font=dfont, bg='white')
label_light = tk.Label(frame, text="Light:", font=dfont, bg='white')
label_lux = tk.Label(frame, textvariable=lux, font=dfont, bg='white')
label_unitlux = tk.Label(frame, text="lux", font=dfont, bg='white')
button_temp = tk.Button(    frame,
text="Toggle Temperature",
font=dfont,
command=toggle_temp)
button_light = tk.Button(   frame,
text="Toggle Light",
font=dfont,
command=toggle_light)
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)
label_unitc.grid(row=1, column=5, sticky=tk.W)
label_light.grid(row=2, column=4, columnspan=2)
label_lux.grid(row=3, column=4, sticky=tk.E)
label_unitlux.grid(row=3, column=5, sticky=tk.W)
button_temp.grid(row=5, column=0, columnspan=2)
button_light.grid(row=5, column=2, columnspan=2)
button_quit.grid(row=5, column=4, columnspan=2)

for w in frame.winfo_children():

# 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('<F11>', toggle_fullscreen)
root.bind('<Escape>', end_fullscreen)

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

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

# Initialize our sensors
tmp102.init()
apds9301.init()

# Call animate() function periodically
fargs = (ax1, ax2, xs, temps, lights, temp_c, lux)
ani = animation.FuncAnimation(  fig,
animate,
fargs=fargs,
interval=update_interval)

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

Save the program (with a fun name like tkinter_dashboard.py), and run it. You should see your sensor data displayed as numerical values as well as a plot that updates once per minute.

Try pushing the "Toggle Temperature" and "Toggle Light" buttons. You should see the graph of each one disappear and reappear with each button press. This demonstrates how you can make an interactive plot using both Tkinter and Matplotlib.

You can update the `update_interval` variable to have the sensors polled more quickly, but it can also be fun to poll once per minute (default) and let it run for a day, as I did in my office:

If you look closely at the graph, you can see that the temperatures fell a little after 7pm, rose again, and then fell once more just before the workday started at 9am the following morning. We can surmise that the building air conditioning was running at those times to make it cooler.

Additionally, you can see that someone came into the office in the 6-7pm timeframe, as the ambient light value picked up for a short amount. Considering I did not move the sensors the next day, it looks like either more lights were on, or it was a sunnier day outside, as more light was falling on the sensor.

Code to Note:

There is a lot going on in this example, so we'll try to cover it as succinctly as possible. Many of the concepts from the previous example, like binding key presses to trigger toggling fullscreen, are still present. Animating a graph is covered in the previous Python tutorial that introduced Matplotlib (specifically, the section about updating a graph in real time). If you are not familiar with Matplotlib, we recommend working through the following tutorial:

### Graph Sensor Data with Python and Matplotlib

#### July 23, 2018

Use matplotlib to create a real-time plot of temperature data collected from a TMP102 sensor connected to a Raspberry Pi.

The key to embedding a Matplotlib graph into a Tkinter GUI is to work with Matplotlib's backend, which is why we import the following:

``````language:python
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
``````

With that, we first create our Matplotlib figure and a set of axes to draw our plot on:

``````language:python
# Create figure for plotting
fig = figure.Figure(figsize=(2, 2))
``````

A few lines later, we create a Tkinter widget out of that figure:

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

These lines use our imported FigureCanvasTkAgg function to take a figure and turn it into a Tkinter Canvas. We get a handle to this canvas and lay it out in our grid just like any other widget:

``````language:python
canvas_plot.grid(   row=0,
column=0,
rowspan=5,
columnspan=4,
sticky=tk.W+tk.E+tk.N+tk.S)
``````

Instead of the periodic `poll()` callback that we used in the previous examples, we set up a `FuncAnimation()` to handle the polling and updating of the graph:

``````language:python
# Call animate() function periodically
fargs = (ax1, ax2, xs, temps, lights, temp_c, lux)
ani = animation.FuncAnimation(  fig,
animate,
fargs=fargs,
interval=update_interval)
``````

In the `animate()` function, we read the sensors' data (just like we did in `poll()`) and append it to the end of some arrays. We use this to redraw the plots on the axes (which are ultimately drawn on the Tkinter canvas widget). Note that we used `.fill_between()` to create the translucent red graph for temperature and a regular `.plot()` to create the basic blue line graph for light value.

For another example on importing Matplotlib into Tkinter, see this demo from the official Matplotlib documentation.