A Simple Kitchen Timer with CircuitPython

A simple program demonstration of object-oriented CircuitPython

Arduino has been a lot of fun, but Python still remains my most favorite programming language. So, it would be inevitable that I venture over to CircuitPython, right?!

Recently, our digital kitchen timer went kaput, so that gave me an excuse to buy a new microcontroller (or two, or three. Hey, they’re so cheap, why not pick up a few!), and become acquainted with new programming environment. This time, I went with Adafruit’s Gemma M0. Round and as small as a quarter, I liked it not only for being so cute, but also for convenient, quick projects. Gemma input/output pins are in the form of sew pads that also fit a 3mm screw, so it’s easy to alligator clip or screw in wires. Breadboards are fun, yes, but this is quite convenient, and the finished assembly is attractive as is, without a need to create a printed circuit board.

So, here’s my simple kitchen timer, with a piezo buzzer wired to the back via screws:

Front side, you’ll notice the dim blue LED – that’s the kitchen timer in its waiting state. Touch one of the pads (yes, capacitive touch!), the LED blinks green, and the timer starts at one minute. Repeated touches add additional minutes. Touch both D1 and D2 simultaneously and the timer resets.

Once alarmed, the LED shows dim green. When the alarm time is reached, the LED starts blinking bright red and the buzzer goes off. Touch one of the pads to stop the alarm. Now the timer is ready for setting again.

I’ve also added a sleep mode to this program, if the alarm isn’t used in a long time, to save on battery; after 5 minutes, the loop slows down and the light dims to yellow. Touch a pad again to wake it back up. This particular microcontroller, doesn’t really have a power management option, though, so this sleep mode doesn’t really help much.

Remaining true to my object-oriented roots, I’ve programmed all this as a Kitchen_Timer class. The microcontroller loop, while True:, just repeatedly calls Kitchen_Timer do_check() method. I see a lot of very linearly-oriented CircuitPython code examples. They’re correct, yes, and efficient, but hard for beginners to read. I’m hoping these object-oriented examples, with device features broken out into discrete methods (play_note, add_time, turn_off, etc.), will allow readers to focus on how each feature works, separate from other code in the microcontroller loop.

import touchio
import board
import adafruit_dotstar
import time
import pulseio
# import alarm # alarm currently works only on the ESP32-S2 chip,
# e.g., AdaFruit's Metro ESP32-S2 and FeatherS2

sleep_time = 5 * 60  # in seconds
addl_time = 60  # in seconds
BLUE = (0, 0, 10)
GREEN = (0, 10, 0)
BRIGHT_GREEN = (0, 255, 0)
OFF = (0, 0, 0)
YELLOW = (10, 10, 0)
LOW_YELLOW = (2, 2, 0)
RED = (255, 0, 0)
# see http://electronic-setup.blogspot.com/2010/11/nokia-rttl-frequencies-hz.html
A5 = 440   # Octave 5 in RTTTL
C5 = 523 
C6 = 1046
A6 = 880
A7 = 1760
E6 = 1175


class Kitchen_Timer():
    def calc_asleep_time(self):
        return time.monotonic() + sleep_time

    def __init__(self, dotstar, piezo):
        # a kitchen timer consists of light (the dotstar) and sound (the piezo)
        # it has touch sensors for input. Those are passed in do_check()
        self.alarm_time = 0
        self.asleep_time = self.calc_asleep_time()
        self.is_asleep = False
        self.alarmed = False
        self.alarming = False
        self.dotstar = dotstar
        self.piezo = piezo
        self.dotstar.brightness = 1
        self.dotstar.fill(BLUE)

    def play_note(self, note):
        if note[0] != 0:
            pwm = pulseio.PWMOut(self.piezo, duty_cycle=0x7FFF, frequency=note[0])
            # 0x7FFF is 50% duty cycle
            # pwm.frequency = math.floor(note[0] * 1.25)
        time.sleep(note[1])
        if note[0] != 0:
            pwm.deinit()

    def play_touch(self):
        self.dotstar.brightness = 8
        self.dotstar.fill(BRIGHT_GREEN)
        self.play_note((E6, 0.125))
        time.sleep(0.2)
        self.dotstar.brightness = 1
        self.dotstar.fill(GREEN)
        time.sleep(0.2)

    def play_beep(self):
        self.play_note((A7, 0.125))

    def play_alarm(self):
        self.dotstar.brightness = 8
        self.dotstar.fill(RED)
        self.play_note((C5, 0.5))
        time.sleep(0.2)
        self.dotstar.fill(OFF)
        time.sleep(0.2)

    def trigger_alarm(self):
        self.alarming = True
        self.alarmed = False
        self.asleep_time = self.calc_asleep_time()
        # reset when we'll go to sleep

    def turn_off(self):
        self.alarming = False
        self.dotstar.brightness = 1
        self.dotstar.fill(BLUE)
        self.play_beep()

    def wake_up(self):
        global loop_sleep
        self.dotstar.fill(BLUE)
        self.play_beep()
        self.is_asleep = False
        loop_sleep = 0.2
        self.asleep_time = self.calc_asleep_time()  # reset our asleep time

    def start(self):
        self.alarm_time = time.monotonic()
        self.alarmed = True

    def cancel(self):
        self.alarmed = False
        self.dotstar.brightness = 1
        self.dotstar.fill(BLUE)
        self.play_beep()

    def add_time(self):
        self.alarm_time += addl_time
        self.play_touch()
        self.dotstar.brightness = 1
        self.dotstar.fill(GREEN)

    def go_to_sleep(self):
        # time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 20)
        self.alarming = False
        self.is_asleep = True
        self.dotstar.fill(LOW_YELLOW)
        # alarm.exit_and_deep_sleep_until_alarms(time_alarm)
        
    def do_check(self, touch1, touch2):
        loop_sleep = 0.2
        if self.alarmed and touch1.value and touch2.value:  # touch both to cancel a pending alarm
            self.cancel()
            time.sleep(0.5)
        if touch1.value or touch2.value:  # touch will either turn off the alarm, reawaken the loop, or add minutes
            if self.alarming:
                self.turn_off()
                time.sleep(0.5)           # gives the user some time to take finger off pad
            elif self.is_asleep:
                self.wake_up()
                time.sleep(0.5)           # just to give some delay before the next touch
            else:                         # we start alarm and add time
                if not self.alarmed:
                    self.start()
                self.add_time()
                time.sleep(0.25)          # delay before the next touch to add time
        if self.alarmed and time.monotonic() > self.alarm_time:
            self.trigger_alarm()
        if self.alarming:
            self.play_alarm()
        if not self.alarmed and not self.is_asleep and time.monotonic() > self.asleep_time:
            loop_sleep = 2.0               # slow down the loop. Not sure if this matters to power management
            self.go_to_sleep()
        return loop_sleep    

touch1 = touchio.TouchIn(board.D1)
touch2 = touchio.TouchIn(board.D2)
dotstar = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1)
alarm = Kitchen_Timer(dotstar=dotstar, piezo=board.D0)

while True:
    loop_time = alarm.do_check(touch1,touch2)    
    time.sleep(loop_time)

Published by

kevin

I'm the founder of Agoric Source, co-organizer of the Houston Python Meetup, director of technology at Newspaper Subscription Services, LP, technology advisor to InstaFuel, active board member of the Houston Area Model United Nations, and occasional volunteer to the Red Cross (during hurricanes or other local emergencies). I'm first and foremost still a software hacker, but with my economics background and business experience, I serve well as a project or program manager, technical visionary, etc.