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)