The CLUE version of this project is a bit more sophisticated than the CPX one, since we have a TFT display to work with and a proximity sensor.
We'll set up the board with CircuitPython and the necessary libraries first, and then code it.
Setup
First, follow this guide on getting CircuitPython and the on your CLUE board. Then, use these instructions to add the libraries you'll need.
Once you've gotten the board set up, click Download: Project Zip below in the code guide. Expand the .zip file and then drag the two .bmp images and the font folder to your CLUE's CIRCUITPY drive via USB.
Code
Copy the code from the code-block below and paste it into the Mu editor and save it to your CLUE as code.py (or copy code.py from the zip file and place on the CIRCUITPY drive).
# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries # # SPDX-License-Identifier: MIT """ Start a 20 second hand washing timer via proximity sensor. Countdown the seconds with text and beeps. Display a bitmaps for waiting and washing modes. """ import time import board from adafruit_clue import clue from adafruit_display_text import label from adafruit_bitmap_font import bitmap_font import displayio import pwmio clue.display.brightness = 0.8 clue_display = displayio.Group() # draw the background image WASH_ON_FILENAME = "wash_on.bmp" # CircuitPython 6 & 7 compatible wash_on_file = open(WASH_ON_FILENAME, "rb") wash_on_bmp = displayio.OnDiskBitmap(wash_on_file) wash_on_sprite = displayio.TileGrid( wash_on_bmp, pixel_shader=getattr(wash_on_bmp, 'pixel_shader', displayio.ColorConverter()) ) # # CircuitPython 7+ compatible # wash_on_bmp = displayio.OnDiskBitmap(WASH_ON_FILENAME) # wash_on_sprite = displayio.TileGrid(wash_on_bmp, pixel_shader=wash_on_bmp.pixel_shader) clue_display.append(wash_on_sprite) # draw the foreground image WASH_OFF_FILENAME = "wash_off.bmp" # CircuitPython 6 & 7 compatible wash_off_file = open(WASH_OFF_FILENAME, "rb") wash_off_bmp = displayio.OnDiskBitmap(wash_off_file) wash_off_sprite = displayio.TileGrid( wash_off_bmp, pixel_shader=getattr(wash_off_bmp, 'pixel_shader', displayio.ColorConverter()) ) # # CircuitPython 7+ compatible # wash_off_bmp = displayio.OnDiskBitmap(WASH_OFF_FILENAME) # wash_off_sprite = displayio.TileGrid(wash_off_bmp, pixel_shader=wash_off_bmp.pixel_shader) clue_display.append(wash_off_sprite) # Create text # first create the group text_group = displayio.Group() # Make a label title_font = bitmap_font.load_font("/font/RacingSansOne-Regular-38.bdf") title_font.load_glyphs("HandWashTimer".encode('utf-8')) title_label = label.Label(title_font, text="Hand Wash", color=0x001122) # Position the label title_label.x = 10 title_label.y = 16 # Append label to group text_group.append(title_label) title2_label = label.Label(title_font, text="Timer", color=0x001122) # Position the label title2_label.x = 6 title2_label.y = 52 # Append label to group text_group.append(title2_label) timer_font = bitmap_font.load_font("/font/RacingSansOne-Regular-29.bdf") timer_font.load_glyphs("0123456789ADSWabcdefghijklmnopqrstuvwxyz:!".encode('utf-8')) timer_label = label.Label(timer_font, text="Wave to start", color=0x4f3ab1) timer_label.x = 24 timer_label.y = 100 text_group.append(timer_label) clue_display.append(text_group) clue.display.root_group = clue_display def countdown(seconds): for i in range(seconds): buzzer.duty_cycle = 2**15 timer_label.text = ("Scrub time: {}".format(seconds-i)) buzzer.duty_cycle = 0 time.sleep(1) timer_label.text = ("Done!") wash_off_sprite.x = 0 buzzer.duty_cycle = 2**15 time.sleep(0.3) buzzer.duty_cycle = 0 timer_label.x = 24 timer_label.y = 100 timer_label.text = ("Wave to start") # setup buzzer buzzer = pwmio.PWMOut(board.SPEAKER, variable_frequency=True) buzzer.frequency = 1000 while True: # print("Distance: {}".format(clue.proximity)) # use to test the sensor if clue.proximity > 1: timer_label.x = 12 timer_label.y = 226 timer_label.text = "Scrub Away!" wash_off_sprite.x = 300 time.sleep(2) countdown(20)
What the code is doing
The code does a few fairly simple things. First, we import the necessary libraries.
import time import board from adafruit_clue import clue from adafruit_display_text import label from adafruit_bitmap_font import bitmap_font import displayio import pulseio
Display and Images
Next, we set the display brightness and then use the clue_display commands to set up the displayio group that will hold the bitmap images and text.
We then draw both images onto the screen, one on top of the other. (The reason we do this is for speed. By loading both images into memory at startup, it will be fast to later move the top image out of the way to reveal the bottom image.)
clue.display.brightness = 0.8 clue_display = displayio.Group() # draw the background image WASH_ON_FILENAME = "wash_on.bmp" # CircuitPython 6 & 7 compatible wash_on_file = open(WASH_ON_FILENAME, "rb") wash_on_bmp = displayio.OnDiskBitmap(wash_on_file) wash_on_sprite = displayio.TileGrid( wash_on_bmp, pixel_shader=getattr(wash_on_bmp, 'pixel_shader', displayio.ColorConverter()) ) # # CircuitPython 7+ compatible # wash_on_bmp = displayio.OnDiskBitmap(WASH_ON_FILENAME) # wash_on_sprite = displayio.TileGrid(wash_on_bmp, pixel_shader=wash_on_bmp.pixel_shader) clue_display.append(wash_on_sprite) # draw the foreground image WASH_OFF_FILENAME = "wash_off.bmp" # CircuitPython 6 & 7 compatible wash_off_file = open(WASH_OFF_FILENAME, "rb") wash_off_bmp = displayio.OnDiskBitmap(wash_off_file) wash_off_sprite = displayio.TileGrid( wash_off_bmp, pixel_shader=getattr(wash_off_bmp, 'pixel_shader', displayio.ColorConverter()) ) # # CircuitPython 7+ compatible # wash_off_bmp = displayio.OnDiskBitmap(WASH_OFF_FILENAME) # wash_off_sprite = displayio.TileGrid(wash_off_bmp, pixel_shader=wash_off_bmp.pixel_shader) clue_display.append(wash_off_sprite)
Text Labels
Then we add groups for each line of text we'll be displaying. This involves specifying bitmap fonts (learn how to make your own in this guide!)
Again, to speed things up later, we'll preload the glyphs (a.k.a., characters) now. Notice that we're loading only the glyphs we intend to use to save time.
We also specify the text to be displayed, the colors, and positions in x/y screen space, and then append each text label to the text_group.
The clue.display.root_group = clue_display
command finally tells the screen to display the bitmaps and text lines.
# Create text # first create the group text_group = displayio.Group() # Make a label title_font = bitmap_font.load_font("/font/RacingSansOne-Regular-38.bdf") title_font.load_glyphs("HandWashTimer".encode('utf-8')) title_label = label.Label(title_font, text="Hand Wash", color=0x001122) # Position the label title_label.x = 10 title_label.y = 16 # Append label to group text_group.append(title_label) title2_label = label.Label(title_font, text="Timer", color=0x001122) # Position the label title2_label.x = 6 title2_label.y = 52 # Append label to group text_group.append(title2_label) timer_font = bitmap_font.load_font("/font/RacingSansOne-Regular-29.bdf") timer_font.load_glyphs("0123456789ADSWabcdefghijklmnopqrstuvwxyz:!".encode('utf-8')) timer_label = label.Label(timer_font, text="Wave to start", color=0x4f3ab1) timer_label.x = 24 timer_label.y = 100 text_group.append(timer_label) clue_display.append(text_group) clue.display.root_group = clue_display
Countdown Function
We'll create a separate function called countdown()
to handle the hand washing countdown process after the CLUE has been triggered.
It will accept an argument for the number of seconds
you intend, which makes it easier to adjust if you like versus a hard coded value.
The function iterates through a loop for the number of seconds
, incrementing the value of i
each time. During each run through this loop it will buzz the buzzer, displaying the scrub time remaining in seconds, pause a second, and repeat.
When the loop finishes the last iteration (20 seconds, say), the wash_off.bmp sprite is moved into place, and the timer label resets to say "Wave to start".
def countdown(seconds): for i in range(seconds): buzzer.duty_cycle = 2**15 timer_label.text = ("Scrub time: {}".format(seconds-i)) buzzer.duty_cycle = 0 time.sleep(1) timer_label.text = ("Done!") wash_off_sprite.x = 0 buzzer.duty_cycle = 2**15 time.sleep(0.3) buzzer.duty_cycle = 0 timer_label.x = 24 timer_label.y = 100 timer_label.text = ("Wave to start")
Buzzer Setup
The buzzer is setup to use the pulsio()
command and buzz the onboard speaker using a pulse wave modulation signal (PWM) set to a frequency of 1000.
When the buzzer.duty_cycle = 2**15
we'll hear the buzzing, and when the duty_cycle = 0
it will stop. These commands are found inside the countdown()
function.
buzzer = pulseio.PWMOut(board.SPEAKER, variable_frequency=True) buzzer.frequency = 1000
Main Loop
Finally, we have finished setting things up and can run the main loop.
Here, we'll use clue.proximity
to check the value of the CLUE's on-board proximity sensor. The value will read 0 when nothing is in front of the sensor. When an object -- such as your hand -- is about 8cm or closer, the sensor will report a value greater than 1 and trip the rest of the conditional statement to run.
In this case, we'll move the timer label to the bottom, replacing the text with "Scrub Away!", move the "off" bitmap out of the way to reveal the hand washing "on" bitmap, wait two seconds and then call the countdown
function for 20 seconds.
while True: # print("Distance: {}".format(clue.proximity)) # use to test the sensor if clue.proximity > 1: timer_label.x = 12 timer_label.y = 226 timer_label.text = "Scrub Away!" wash_off_sprite.x = 300 time.sleep(2) countdown(20)
Now, you can mount the CLUE over your sink, run a USB power cable to it from an outlet, and wash your hands to a stylish timer!
Page last edited February 24, 2025
Text editor powered by tinymce.