Once you've finished setting up your QT Py ESP32-S3 with CircuitPython, you can access the code, audio files and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
# SPDX-License-Identifier: MIT
'''LED Matrix Alarm Clock with Scrolling Wake Up Text and Winking Eyes'''
import os
import ssl
import time
import random
import wifi
import socketpool
import microcontroller
import board
import audiocore
import audiobusio
import audiomixer
import adafruit_is31fl3741
from adafruit_is31fl3741.adafruit_rgbmatrixqt import Adafruit_RGBMatrixQT
import adafruit_ntp
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
from rainbowio import colorwheel
from adafruit_seesaw import digitalio, rotaryio, seesaw
from adafruit_debouncer import Button
# Configuration
timezone = -4
alarm_hour = 11
alarm_min = 36
alarm_volume = .2
hour_12 = True
no_alarm_plz = False
BRIGHTNESS_DAY = 200
BRIGHTNESS_NIGHT = 50
# I2S pins for Audio BFF
DATA = board.A0
LRCLK = board.A1
BCLK = board.A2
# Connect to WIFI
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
context = ssl.create_default_context()
pool = socketpool.SocketPool(wifi.radio)
ntp = adafruit_ntp.NTP(pool, tz_offset=timezone, cache_seconds=3600)
# Initialize I2C and displays
i2c = board.STEMMA_I2C()
matrix1 = Adafruit_RGBMatrixQT(i2c, address=0x30, allocate=adafruit_is31fl3741.PREFER_BUFFER)
matrix2 = Adafruit_RGBMatrixQT(i2c, address=0x31, allocate=adafruit_is31fl3741.PREFER_BUFFER)
# Configure displays
for m in [matrix1, matrix2]:
m.global_current = 0x05
m.set_led_scaling(BRIGHTNESS_DAY)
m.enable = True
m.fill(0x000000)
m.show()
# Audio setup
audio = audiobusio.I2SOut(BCLK, LRCLK, DATA)
wavs = ["/"+f for f in os.listdir('/') if f.lower().endswith('.wav') and not f.startswith('.')]
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=32768)
mixer.voice[0].level = alarm_volume
audio.play(mixer)
def open_audio():
"""Open a random WAV file"""
filename = random.choice(wavs)
return audiocore.WaveFile(open(filename, "rb"))
def update_brightness(hour_24):
"""Update LED brightness based on time of day"""
brightness = BRIGHTNESS_NIGHT if (hour_24 >= 20 or hour_24 < 7) else BRIGHTNESS_DAY
matrix1.set_led_scaling(brightness)
matrix2.set_led_scaling(brightness)
return brightness
# Seesaw setup for encoder and button
seesaw = seesaw.Seesaw(i2c, addr=0x36)
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
button = Button(digitalio.DigitalIO(seesaw, 24), long_duration_ms=1000)
encoder = rotaryio.IncrementalEncoder(seesaw)
last_position = 0
# Font definitions
FONT_5X7 = {
'0': [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
'1': [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
'2': [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
'3': [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
'4': [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
'5': [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
'6': [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
'7': [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
'8': [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
'9': [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
' ': [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
'W': [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
'A': [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
'K': [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
'E': [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
'U': [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
'P': [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
'O': [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
'N': [0b10001, 0b11001, 0b10101, 0b10101, 0b10011, 0b10001, 0b10001],
'F': [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000]
}
# Eye patterns
EYE_OPEN = [0b10101, 0b01110, 0b10001, 0b10101, 0b10001, 0b01110, 0b00000]
EYE_CLOSED = [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000]
class Display:
"""Handle all display operations"""
def __init__(self, m1, m2):
self.matrix1 = m1
self.matrix2 = m2
def clear(self):
"""Clear both displays"""
self.matrix1.fill(0x000000)
self.matrix2.fill(0x000000)
def show(self):
"""Update both displays"""
self.matrix1.show()
self.matrix2.show()
def pixel(self, matrix, x, y, color): # pylint: disable=no-self-use
"""Draw a pixel with 180-degree rotation"""
fx, fy = 12 - x, 8 - y
if 0 <= fx < 13 and 0 <= fy < 9:
matrix.pixel(fx, fy, color)
def draw_char(self, matrix, char, x, y, color):
"""Draw a character at position x,y"""
if char.upper() in FONT_5X7:
bitmap = FONT_5X7[char.upper()]
for row in range(7):
for col in range(5):
if bitmap[row] & (1 << (4 - col)):
self.pixel(matrix, x + col, y + row, color)
def draw_colon(self, y, color, is_pm=False):
"""Draw colon split between displays with optional PM indicator"""
# Two dots for the colon
for dy in [(1, 2), (4, 5)]:
for offset in dy:
self.pixel(self.matrix1, 12, y + offset, color)
self.pixel(self.matrix2, 0, y + offset, color)
# PM indicator dot
if is_pm:
self.pixel(self.matrix1, 12, y + 6, color)
self.pixel(self.matrix2, 0, y + 6, color)
def draw_time(self, time_str, color, is_pm=False):
"""Draw time display across both matrices"""
self.clear()
y = 1
# Draw digits
if len(time_str) >= 5:
self.draw_char(self.matrix1, time_str[0], 0, y, color)
self.draw_char(self.matrix1, time_str[1], 6, y, color)
self.draw_colon(y, color, is_pm)
self.draw_char(self.matrix2, time_str[3], 2, y, color)
self.draw_char(self.matrix2, time_str[4], 8, y, color)
self.show()
def draw_scrolling_text(self, text, offset, color):
"""Draw scrolling text across both matrices"""
self.clear()
char_width = 6
total_width = 26
# Calculate position for smooth scrolling
y = 1
for i, char in enumerate(text):
# Start from right edge and move left
char_x = total_width - offset + (i * char_width)
# Draw character if any part is visible
if -6 < char_x < total_width:
if char_x < 13: # On matrix1
self.draw_char(self.matrix1, char, char_x, y, color)
else: # On matrix2
self.draw_char(self.matrix2, char, char_x - 13, y, color)
self.show()
def draw_eye(self, matrix, pattern, color):
"""Draw eye pattern centered on matrix"""
x, y = 4, 1 # Center position
for row in range(7):
for col in range(5):
if pattern[row] & (1 << (4 - col)):
self.pixel(matrix, x + col, y + row, color)
def wink_animation(self, color):
"""Perform winking animation"""
# Sequence: open -> left wink -> open -> right wink -> open
sequences = [
(EYE_OPEN, EYE_OPEN),
(EYE_CLOSED, EYE_OPEN),
(EYE_OPEN, EYE_OPEN),
(EYE_OPEN, EYE_CLOSED),
(EYE_OPEN, EYE_OPEN)
]
for left_eye, right_eye in sequences:
self.clear()
self.draw_eye(self.matrix1, left_eye, color)
self.draw_eye(self.matrix2, right_eye, color)
self.show()
time.sleep(0.3)
def blink_time(self, time_str, color, is_pm=False, count=3):
"""Blink time display for mode changes"""
for _ in range(count):
self.clear()
self.show()
time.sleep(0.2)
self.draw_time(time_str, color, is_pm)
time.sleep(0.2)
# Initialize display handler
display = Display(matrix1, matrix2)
# State variables
class State:
"""Track all state variables"""
def __init__(self):
self.color_value = 0
self.color = colorwheel(0)
self.is_pm = False
self.alarm_is_pm = False
self.time_str = "00:00"
self.set_alarm = 0
self.active_alarm = False
self.alarm_str = f"{alarm_hour:02}:{alarm_min:02}"
self.current_brightness = BRIGHTNESS_DAY
# Timers
self.refresh_timer = Timer(3600000) # 1 hour
self.clock_timer = Timer(1000) # 1 second
self.wink_timer = Timer(30000) # 30 seconds
self.scroll_timer = Timer(80) # Scroll speed
self.blink_timer = Timer(500) # Blink speed
self.alarm_status_timer = Timer(100) # Status scroll
# Display state
self.scroll_offset = 0
self.blink_state = True
self.showing_status = False
self.status_start_time = 0
self.alarm_start_time = 0
# Time tracking
self.first_run = True
self.seconds = 0
self.mins = 0
self.am_pm_hour = 0
class Timer:
"""Simple timer helper"""
def __init__(self, interval):
self.interval = interval
self.last_tick = ticks_ms()
def check(self):
"""Check if timer has elapsed"""
if ticks_diff(ticks_ms(), self.last_tick) >= self.interval:
self.last_tick = ticks_add(self.last_tick, self.interval)
return True
return False
def reset(self):
"""Reset timer"""
self.last_tick = ticks_ms()
# Initialize state
state = State()
def format_time_display(hour_24, minute, use_12hr=True):
"""Format time for display with AM/PM detection"""
if use_12hr:
hour = hour_24 % 12
if hour == 0:
hour = 12
is_pm = hour_24 >= 12
else:
hour = hour_24
is_pm = False
return f"{hour:02}:{minute:02}", is_pm
def sync_time():
"""Sync with NTP server"""
try:
print("Getting time from internet!")
now = ntp.datetime
state.am_pm_hour = now.tm_hour
state.mins = now.tm_min
state.seconds = now.tm_sec
state.time_str, state.is_pm = format_time_display(state.am_pm_hour, state.mins, hour_12)
update_brightness(state.am_pm_hour)
if not state.active_alarm and not state.showing_status:
display.draw_time(state.time_str, state.color, state.is_pm)
print(f"Time: {state.time_str}")
state.first_run = False
return True
except Exception as e: # pylint: disable=broad-except
print(f"Error syncing time: {e}")
return False
# Main loop
while True:
button.update()
# Handle button presses
if button.long_press:
if state.set_alarm == 0 and not state.active_alarm:
# Enter alarm setting mode
state.blink_timer.reset()
state.set_alarm = 1
state.alarm_is_pm = alarm_hour >= 12 if hour_12 else False
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.blink_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
# Draw the alarm hour after blinking to keep it displayed
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
elif state.active_alarm:
# Stop alarm
mixer.voice[0].stop()
state.active_alarm = False
update_brightness(state.am_pm_hour)
state.scroll_offset = 0
# Immediately redraw the current time
display.draw_time(state.time_str, state.color, state.is_pm)
print("Alarm silenced")
if button.short_count == 1: # Changed from == 1 to >= 1 for better detection
# Cycle through alarm setting modes
state.set_alarm = (state.set_alarm + 1) % 3
if state.set_alarm == 0:
# Exiting alarm setting mode - redraw current time
state.wink_timer.reset()
display.draw_time(state.time_str, state.color, state.is_pm)
elif state.set_alarm == 1:
# Entering hour setting
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
# Reset timer to prevent immediate blinking
elif state.set_alarm == 2:
# Entering minute setting
display.blink_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
# Draw the minutes after blinking to keep them displayed
display.draw_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
# Reset timer to prevent immediate blinking
if button.short_count == 3: # Changed for better detection
# Toggle alarm on/off
no_alarm_plz = not no_alarm_plz
print(f"Alarm disabled: {no_alarm_plz}")
state.showing_status = True
state.status_start_time = ticks_ms()
state.scroll_offset = 0
# Handle encoder (your existing code)
position = -encoder.position
if position != last_position:
delta = 1 if position > last_position else -1
if state.set_alarm == 0:
# Change color
state.color_value = (state.color_value + delta * 5) % 255
state.color = colorwheel(state.color_value)
display.draw_time(state.time_str, state.color, state.is_pm)
elif state.set_alarm == 1:
# Change hour
alarm_hour = (alarm_hour + delta) % 24
state.alarm_is_pm = alarm_hour >= 12 if hour_12 else False
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
elif state.set_alarm == 2:
# Change minute
alarm_min = (alarm_min + delta) % 60
display.draw_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
state.alarm_str = f"{alarm_hour:02}:{alarm_min:02}"
last_position = position
# Handle alarm status display
if state.showing_status:
if state.alarm_status_timer.check():
status_text = "OFF " if no_alarm_plz else "ON "
display.draw_scrolling_text(status_text, state.scroll_offset, state.color)
text_width = 4*6 if no_alarm_plz else 3*6
state.scroll_offset += 1
# Reset when text has completely scrolled off
if state.scroll_offset > text_width + 18:
state.scroll_offset = 0
state.showing_status = False
if state.set_alarm == 0 and not state.active_alarm:
display.draw_time(state.time_str, state.color, state.is_pm)
# Handle active alarm scrolling
if state.active_alarm:
# Auto-silence alarm after 1 minute
if ticks_diff(ticks_ms(), state.alarm_start_time) >= 60000:
mixer.voice[0].stop()
state.active_alarm = False
update_brightness(state.am_pm_hour)
state.scroll_offset = 0
display.draw_time(state.time_str, state.color, state.is_pm)
print("Alarm auto-silenced")
elif state.scroll_timer.check():
display.draw_scrolling_text("WAKE UP ", state.scroll_offset, state.color)
text_width = 8 * 6 # "WAKE UP " is 8 characters
state.scroll_offset += 1
# Reset when text has completely scrolled off
if state.scroll_offset > text_width + 26:
state.scroll_offset = 0
# Handle alarm setting mode blinking
elif state.set_alarm > 0:
# Only blink if enough time has passed since mode change
if state.blink_timer.check():
state.blink_state = not state.blink_state
if state.blink_state:
# Redraw during the "on" part of blink
if state.set_alarm == 1:
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
else:
display.draw_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
else:
# Only clear display during the "off" part of blink
display.clear()
display.show()
# Normal mode operations
else: # state.set_alarm == 0
# Winking animation
if not state.active_alarm and not state.showing_status and state.wink_timer.check():
print("Winking!")
display.wink_animation(state.color)
display.draw_time(state.time_str, state.color, state.is_pm)
# Time sync
if state.refresh_timer.check() or state.first_run:
if not sync_time():
time.sleep(10)
microcontroller.reset()
# Local timekeeping
if state.clock_timer.check():
state.seconds += 1
if state.seconds > 59:
state.seconds = 0
state.mins += 1
if state.mins > 59:
state.mins = 0
state.am_pm_hour = (state.am_pm_hour + 1) % 24
update_brightness(state.am_pm_hour)
# Update display
state.time_str, state.is_pm = format_time_display(state.am_pm_hour,
state.mins, hour_12)
if not state.active_alarm and not state.showing_status:
display.draw_time(state.time_str, state.color, state.is_pm)
# Check alarm
if f"{state.am_pm_hour:02}:{state.mins:02}" == state.alarm_str and not no_alarm_plz:
print("ALARM!")
wave = open_audio()
mixer.voice[0].play(wave, loop=True)
state.active_alarm = True
state.alarm_start_time = ticks_ms()
state.scroll_offset = 0
Upload the Code and Libraries to the QT Py ESP32-S3
After downloading the Project Bundle, plug your QT Py ESP32-S3 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the QT Py ESP32-S3's CIRCUITPY drive.
- lib folder
- nice-alarm.wav
- square-alarm.wav
- code.py
Your QT Py ESP32-S3 CIRCUITPY drive should look like this after copying the lib folder, two .WAV files and code.py file:
Add Your settings.toml File
As of CircuitPython 8.0.0, there is support for Environment Variables. Environment variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include values for your CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD.
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"
How the CircuitPython Code Works
At the top of the code are user configurable settings for the clock. You'll set your timezone, alarm time, alarm volume, 12 hour vs. 24 hour time and LED brightness for day and night.
# Configuration timezone = -4 alarm_hour = 11 alarm_min = 36 alarm_volume = .2 hour_12 = True no_alarm_plz = False BRIGHTNESS_DAY = 200 BRIGHTNESS_NIGHT = 50
# Connect to WIFI
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
context = ssl.create_default_context()
pool = socketpool.SocketPool(wifi.radio)
ntp = adafruit_ntp.NTP(pool, tz_offset=timezone, cache_seconds=3600)
LEDs
The two RGB matrix displays connect over I2C. One is on the default address (0x30) and the second is on address 0x31.
# Initialize I2C and displays
i2c = board.STEMMA_I2C()
matrix1 = Adafruit_RGBMatrixQT(i2c, address=0x30, allocate=adafruit_is31fl3741.PREFER_BUFFER)
matrix2 = Adafruit_RGBMatrixQT(i2c, address=0x31, allocate=adafruit_is31fl3741.PREFER_BUFFER)
# Configure displays
for m in [matrix1, matrix2]:
m.global_current = 0x05
m.set_led_scaling(BRIGHTNESS_DAY)
m.enable = True
m.fill(0x000000)
m.show()
Audio
.WAV files are played when the alarm goes off on the clock. Any .WAV file that is added to the CIRCUITPY drive is stored in the wavs array. When an alarm is triggered, one of the .WAV files is opened and played on a loop.
# Audio setup
audio = audiobusio.I2SOut(BCLK, LRCLK, DATA)
wavs = ["/"+f for f in os.listdir('/') if f.lower().endswith('.wav') and not f.startswith('.')]
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=32768)
mixer.voice[0].level = alarm_volume
audio.play(mixer)
def open_audio():
"""Open a random WAV file"""
filename = random.choice(wavs)
return audiocore.WaveFile(open(filename, "rb"))
seesaw
The rotary encoder is instantiated over I2C. The button on the encoder is passed to a debouncer Button object. This lets you use long press and short press detection.
# Seesaw setup for encoder and button seesaw = seesaw.Seesaw(i2c, addr=0x36) seesaw.pin_mode(24, seesaw.INPUT_PULLUP) button = Button(digitalio.DigitalIO(seesaw, 24), long_duration_ms=1000) encoder = rotaryio.IncrementalEncoder(seesaw) last_position = 0
Custom Display Class
A custom 5x7 font, eye bitmaps and Display class is used for the LED matrices. The custom class takes care of drawing characters, animating the eyes, scrolling text and rotating the displays in software.
# Eye patterns
EYE_OPEN = [0b10101, 0b01110, 0b10001, 0b10101, 0b10001, 0b01110, 0b00000]
EYE_CLOSED = [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000]
class Display:
"""Handle all display operations"""
def __init__(self, m1, m2):
self.matrix1 = m1
self.matrix2 = m2
State Tracking
A State class takes care of tracking all of the different states, modes and timers used in the loop.
# State variables
class State:
"""Track all state variables"""
def __init__(self):
self.color_value = 0
self.color = colorwheel(0)
self.is_pm = False
self.alarm_is_pm = False
self.time_str = "00:00"
self.set_alarm = 0
self.active_alarm = False
self.alarm_str = f"{alarm_hour:02}:{alarm_min:02}"
self.current_brightness = BRIGHTNESS_DAY
# Timers
self.refresh_timer = Timer(3600000) # 1 hour
self.clock_timer = Timer(1000) # 1 second
self.wink_timer = Timer(30000) # 30 seconds
self.scroll_timer = Timer(80) # Scroll speed
self.blink_timer = Timer(500) # Blink speed
self.alarm_status_timer = Timer(100) # Status scroll
# Display state
self.scroll_offset = 0
self.blink_state = True
self.showing_status = False
self.status_start_time = 0
self.alarm_start_time = 0
# Time tracking
self.first_run = True
self.seconds = 0
self.mins = 0
self.am_pm_hour = 0
The Loop
In the loop, the button is tracked to determine if a long press or short press is received. A long press lets you set a new alarm on the clock or turn off an active alarm. A single short press lets you navigate the alarm setting. Three short presses in a row lets you toggle the alarm on or off.
# Main loop
while True:
button.update()
# Handle button presses
if button.long_press:
if state.set_alarm == 0 and not state.active_alarm:
# Enter alarm setting mode
state.blink_timer.reset()
state.set_alarm = 1
state.alarm_is_pm = alarm_hour >= 12 if hour_12 else False
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.blink_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
# Draw the alarm hour after blinking to keep it displayed
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
elif state.active_alarm:
# Stop alarm
mixer.voice[0].stop()
state.active_alarm = False
update_brightness(state.am_pm_hour)
state.scroll_offset = 0
# Immediately redraw the current time
display.draw_time(state.time_str, state.color, state.is_pm)
print("Alarm silenced")
if button.short_count == 1:
# Cycle through alarm setting modes
state.set_alarm = (state.set_alarm + 1) % 3
if state.set_alarm == 0:
# Exiting alarm setting mode - redraw current time
state.wink_timer.reset()
display.draw_time(state.time_str, state.color, state.is_pm)
elif state.set_alarm == 1:
# Entering hour setting
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
# Reset timer to prevent immediate blinking
elif state.set_alarm == 2:
# Entering minute setting
display.blink_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
# Draw the minutes after blinking to keep them displayed
display.draw_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
# Reset timer to prevent immediate blinking
if button.short_count == 3:
# Toggle alarm on/off
no_alarm_plz = not no_alarm_plz
print(f"Alarm disabled: {no_alarm_plz}")
state.showing_status = True
state.status_start_time = ticks_ms()
state.scroll_offset = 0
Encoder
The encoder lets you change the color of the RGB LEDs. It cycles through the rainbow. When you are setting a new alarm, the encoder lets you rotate through the hours and minutes.
# Handle encoder (your existing code)
position = -encoder.position
if position != last_position:
delta = 1 if position > last_position else -1
if state.set_alarm == 0:
# Change color
state.color_value = (state.color_value + delta * 5) % 255
state.color = colorwheel(state.color_value)
display.draw_time(state.time_str, state.color, state.is_pm)
elif state.set_alarm == 1:
# Change hour
alarm_hour = (alarm_hour + delta) % 24
state.alarm_is_pm = alarm_hour >= 12 if hour_12 else False
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
elif state.set_alarm == 2:
# Change minute
alarm_min = (alarm_min + delta) % 60
display.draw_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
state.alarm_str = f"{alarm_hour:02}:{alarm_min:02}"
last_position = position
Text on a Clock
When you toggle the alarm with three short button presses, "ON" or "OFF" scrolls across the displays to let you know if the alarm is on or off.
# Handle alarm status display
if state.showing_status:
if state.alarm_status_timer.check():
status_text = "OFF " if no_alarm_plz else "ON "
display.draw_scrolling_text(status_text, state.scroll_offset, state.color)
text_width = 4*6 if no_alarm_plz else 3*6
state.scroll_offset += 1
# Reset when text has completely scrolled off
if state.scroll_offset > text_width + 18:
state.scroll_offset = 0
state.showing_status = False
if state.set_alarm == 0 and not state.active_alarm:
display.draw_time(state.time_str, state.color, state.is_pm)
When an alarm is active, "WAKE UP" scrolls across the displays.
# Handle active alarm scrolling
if state.active_alarm:
# Auto-silence alarm after 1 minute
if ticks_diff(ticks_ms(), state.alarm_start_time) >= 60000:
mixer.voice[0].stop()
state.active_alarm = False
update_brightness(state.am_pm_hour)
state.scroll_offset = 0
display.draw_time(state.time_str, state.color, state.is_pm)
print("Alarm auto-silenced")
elif state.scroll_timer.check():
display.draw_scrolling_text("WAKE UP ", state.scroll_offset, state.color)
text_width = 8 * 6 # "WAKE UP " is 8 characters
state.scroll_offset += 1
# Reset when text has completely scrolled off
if state.scroll_offset > text_width + 26:
state.scroll_offset = 0
# Handle alarm setting mode blinking
elif state.set_alarm > 0:
# Only blink if enough time has passed since mode change
if state.blink_timer.check():
state.blink_state = not state.blink_state
if state.blink_state:
# Redraw during the "on" part of blink
if state.set_alarm == 1:
hour_str, _ = format_time_display(alarm_hour, 0, hour_12)
display.draw_time(hour_str[:2] + ": ", state.color, state.alarm_is_pm)
else:
display.draw_time(f" :{alarm_min:02}", state.color, state.alarm_is_pm)
else:
# Only clear display during the "off" part of blink
display.clear()
display.show()
Eyes on a Clock
When the clock is just being a clock, you'll see the blinking eye animation every 30 seconds.
else: # state.set_alarm == 0
# Winking animation
if not state.active_alarm and not state.showing_status and state.wink_timer.check():
print("Winking!")
display.wink_animation(state.color)
display.draw_time(state.time_str, state.color, state.is_pm)
Finally, the Clock Code
Every hour, the clock syncs with the NTP server to make sure that the time is accurate. Between syncs, time is kept locally on the QT Py using ticks().
# Time sync
if state.refresh_timer.check() or state.first_run:
if not sync_time():
time.sleep(10)
microcontroller.reset()
# Local timekeeping
if state.clock_timer.check():
state.seconds += 1
if state.seconds > 59:
state.seconds = 0
state.mins += 1
if state.mins > 59:
state.mins = 0
state.am_pm_hour = (state.am_pm_hour + 1) % 24
update_brightness(state.am_pm_hour)
# Update display
state.time_str, state.is_pm = format_time_display(state.am_pm_hour,
state.mins, hour_12)
if not state.active_alarm and not state.showing_status:
display.draw_time(state.time_str, state.color, state.is_pm)
# Check alarm
if f"{state.am_pm_hour:02}:{state.mins:02}" == state.alarm_str and not no_alarm_plz:
print("ALARM!")
wave = open_audio()
mixer.voice[0].play(wave, loop=True)
state.active_alarm = True
state.alarm_start_time = ticks_ms()
state.scroll_offset = 0
Page last edited June 09, 2025
Text editor powered by tinymce.