Maloney Home Page  |  Four-item kitchen timer

As practice in writing object-oriented code in Python, I made a four-item kitchen timer with touch-activated controls on a tablet. (I greatly appreciate guidance from my colleagues Irene and Josh Bialkowski; any remaining awkward code is mine.)

The requirement were as follows:

  • Four spaces are provided for individual timers. We offer the possibility to label each timer as Meat, Vegetable, Grain, or Miscellaneous. Categories can be used more than once (e.g., there could be two grain timers counting down simultaneously)
  • The user selects the food type, enters the time in minutes using a graphical number pad, and launches the timer by touching Enter. Any timer can be closed by tapping it. It should not be possible to violate this sequence or crash the program (e.g., when entering the time, the food selection buttons and the timer close buttons should be inoperative.)
  • Timers should have a resolution of seconds. Timing must be performed objectively, with no chance of deviation from high CPU processing rates, for example. The background color should change to yellow (warning) at a certain time and red (alert) at a certain time. After reaching zero, the counter should count up.
  • The program can operate indefinitely; i.e., timers can be closed in any order, and new timers are properly positioned in the next available space.
  • A 12-hour clock is also shown.

We begin in Python by importing essential modules, including the tkinter module, which (annoyingly) is capitalized differently in different versions:

#!/usr/bin/env python
import time
try:
    # if running Python2
    import Tkinter as tk
    import tkFont
except ImportError:
    # if running Python3
    import tkinter as tk
    import tkinter.font as tkFont
from functools import partial # used to pass arguments in button commands

We initialize several variables, including counters, times, and fonts:

# some initial variables
time_entry_value = 0
active_timer = 0
warning_yellow = 30 # threshold for turning yellow, in seconds
warning_red = 0 # threshold for turning run, in seconds
# font assignments
clock_font = ('arial', 156, 'bold')
frame_font = ('arial', 40, 'bold')
pad_font = ('arial', 60, 'bold')
timer_font = ('arial', 160, 'bold')

I decided to create two types of classes to implement Python's object-oriented capabilities. The first (Parameters) comprises the two types of user input (namely, the food type and new timer duration). The second (Timer) comprises the up-to-four timers displayed in four corners of the screen.

class Parameters(object):
    """Define a class for the food and time frames and buttons"""
    def __init__(self, name=None, time=None, frame=None, button=None):
        self.name = name
        self.time = time
        self.frame = frame
        self.button = []

We now instantiate the two Parameter types:

# assign next food type    
food = Parameters()
food.name = ['Meat','Veg','Grain','Misc']

# number pad for setting next timer duration
duration = Parameters()
duration.name = [
'1',  '2',  '3', 
'4',  '5',  '6', 
'7',  '8',  '9', 
'0' ]

We define the Timer class:

class Timer(object):
    """Define a class for the individual timers"""
    def __init__(self, time=None, remaining=None, frame=None, button=None, operating=None):
        self.time = time
        self.remaining = remaining
        self.frame = []
        self.button = []
        self.operating = 0
    # this subroutine counts down the time for the individual timers    
    def implement_timer(self, timer_entry=None):
        if timer_entry is not None: # at first call, assign the remaining time
            self.remaining = timer_entry
        string_time = '{:02d}:{:02d}'.format(*divmod(int(self.remaining), 60)) # pad with zeros
        if self.remaining <= warning_red: 
            self.button.config(bg='red') # change color when timer goes under a threshold value
            string_time = '{:02d}:{:02d}'.format(*divmod(int(-self.remaining), 60)) # after time expires, start counting up
        elif self.remaining <= warning_yellow: 
            self.button.config(bg='yellow')
        self.button.config(text=string_time) # update the button
        self.remaining += -0.2 # decrement and use "after" to wait
        if self.operating is True: # this lets timer be turned off by flipping the flag to false
            self.button.after(200,self.implement_timer)
        else:
            self.frame.config(text='Ready', fg='gray70') # if timer is turned off
            self.button.config(text=' ', bg=bg, state=tk.DISABLED)

And we define a few functions for potential actions:

# the first step is to choose a food type
def food_entry(i):
    # turn off the food buttons and turn on the time buttons
    for j in range(len(food.button)):
        food.button[j].config(state=tk.DISABLED)
    for j in range(len(duration.button)):
        duration.button[j].config(state=tk.NORMAL)
    # disable timer close (fixes bug in which user closes a timer while another is being configured)    
    for j in range(len(timer_list)):    
        timer_list[j].button.config(command='')
    # activate the next available timer box
    timer_list[active_timer].frame.config(text=food.name[i], fg='black')       
    timer_list[active_timer].button.config(state=tk.NORMAL, text='00:00', fg='black')
    
# the second step is to enter a time
def keypad_entry(i):
    global active_timer
    global time_entry_value
    # "Enter" starts the timer; anything else adds a digit
    if i == 'Enter':
        # turn off the time buttons
        for j in range(len(duration.button)):
            duration.button[j].config(state=tk.DISABLED)
        # reactivate timer close
        for j in range(len(timer_list)):    
            timer_list[j].button.config(command=partial(timer_close,j))
        # flip the "operating" tag and pass through the time in seconds
        timer_list[active_timer].operating = 1
        timer_list[active_timer].implement_timer(time_entry_value * 60)
        # reset the time for the next timer
        time_entry_value = 0
        # figure out where the next timer will go
        active_timer=find_element(0, [timer_list[i].operating for i in range(len(timer_list))])
        # if there's space for another timer, enable the food selection buttons again;
        # otherwise, need to wait for a timer to be closed
        if active_timer is not None:
            for j in range(len(food.button)):
                food.button[j].config(state=tk.NORMAL)
    # add a digit
    else:
        time_entry_value = 10 * time_entry_value + int(i)
        timer_list[active_timer].button.config(text=str('%02d' % time_entry_value)+':00')

# when a timer is closed, flip the "operating" flag, reactivate the food selection buttons,
# and reidentify the next appropriate timer location
def timer_close(i):
    global active_timer
    timer_list[i].operating = 0
    for j in range(len(food.button)):
        food.button[j].config(state=tk.NORMAL)
    active_timer=find_element(0, [timer_list[i].operating for i in range(len(timer_list))])
    
# pick the next timer location by finding the first example of an element in a list
def find_element(element, list_element):
    try:
        index_element = list_element.index(element)
        return index_element
    except ValueError:
        return None

We can now create the graphical window, set it to fill the screen, and obtain the details of the background color for use when coloring the timers:

root = tk.Tk()
ws, hs = root.winfo_screenwidth(), root.winfo_screenheight()
print(ws,hs)
root.geometry("%dx%d+0+0" % (ws, hs+40))
root.attributes('-fullscreen', True)
bg = root.cget('bg')
root.title('')

Having defined the input and output classes and the key functions and set up the window, we arrange the frames as follows:

clock_and_timers_frame = tk.Frame(root)
clock_and_timers_frame.pack(side=tk.LEFT, padx=20)
controls_frame = tk.Frame(root)
controls_frame.pack(side=tk.LEFT, padx=10)
clock_frame = tk.Frame(clock_and_timers_frame)
clock_frame.pack(side=tk.TOP, ipady=26)
timer_frame = tk.Frame(clock_and_timers_frame)
timer_frame.pack(fill='x', expand=True, padx=20)
controls_food_frame=tk.Frame(controls_frame)
controls_food_frame.pack(fill='y', pady=50)
controls_time_frame=tk.Frame(controls_frame)
controls_time_frame.pack(fill='x')

# configure the food selection frame and buttons
foodpad = tk.LabelFrame(controls_food_frame, font=frame_font, text="food entry", bd=3)
foodpad.pack()
for i in range(len(food.name)):
    food.button.append(tk.Button(foodpad, font=pad_font, text=food.name[i], 
        command=partial(food_entry,i), width=6))
    food.button[i].grid(row=i,column=0)

# configure the time selection frame and buttons
duration.frame = tk.LabelFrame(controls_time_frame, font=frame_font, text="duration entry", bd=3)
duration.frame.pack(padx=0, pady=0)
duration.button = list(range(len(duration.name)))
# start with time buttons grayed out
for i in range(len(duration.name)):
    duration.button[i] = tk.Button(duration.frame, font=pad_font, text=duration.name[i], width=3, 
        command=partial(keypad_entry, duration.name[i]), state=tk.DISABLED)
    duration.button[i].grid(row=i // 3, column=i % 3)
# add the "Enter" button
duration.button.append(tk.Button(duration.frame, font=pad_font, text='Enter', width=7, 
    command=partial(keypad_entry, 'Enter'), state=tk.DISABLED))
duration.button[-1].grid(row=3, column=1, columnspan=2)

Now we add the top clock:

# get the current local time from the PC, strip leading zeros
def tick(time1=''):
    time2 = time.strftime('%I:%M:%S %p').lstrip("0").replace(" 0", " ")
    # if the time string has changed, update it
    if time2 != time1:
        time1 = time2
        clock.config(text=time2)
    # calls itself every 200 milliseconds to update the time display as needed
    clock.after(200, tick)
clock = tk.Label(clock_frame, font=clock_font)
clock.pack()
tick()

Finally, we instantiate the four potential Timer types and start the loop:

timer_list = ['NW', 'NE', 'SW', 'SE']
for i in range(len(timer_list)):
    timer_list[i] = Timer()
    timer_list[i].frame = (tk.LabelFrame(timer_frame, font=frame_font, text='Ready', fg='gray70', bd=3))
    timer_list[i].frame.grid(row=i // 2, column= i % 2, pady=20)
    timer_list[i].button = (tk.Button(timer_list[i].frame, font=timer_font, text=' ', 
        width=5, height=1, command=partial(timer_close, i), state=tk.DISABLED))
    timer_list[i].button.pack(ipady=30)

# start the loop
root.mainloop()
To prevent the screen saver on the tablet from dimming or turning off the screen, I run "Nosleep.exe", which programmatically moves the cursor by a pixel or so every so often. Then, the batch file to run the program (which first starts XWin, necessary for Cygwin, and waits a few seconds for it to launch) is as follows:
c:\cygwin\bin\run.exe --quote /usr/bin/bash.exe -l -c "cd; exec /usr/bin/startxwin"
timeout 5
set PATH=C:\cygwin\bin;%PATH%
c:\cygwin\bin\bash c:\users\johnm_000\dropbox\python\Timer.sh

and the called .sh file reads as follows:

#!/bin/sh
export DISPLAY=:0.0
python c:/users/johnm_000/dropbox/python/timer.py

where timer.py is the file presented above.

Of course, it wasn’t long after coding this up that I got comfortable enough with a new set of recipes that I could approximate things by taste, sight, or feel. (Except for boiling eggs. Maybe I need to update the food list to include Egg).