PyBadge as a CircuitPython Learning Platform
The PyBadge is one of the most feature‑packed ways to experiment and learn CircuitPython. With an integrated TFT display, eight buttons, five NeoPixels, a light and an acceleration sensor, audio with an on-board speaker and amplifier for an external speaker, LiPo charging, and multiple expansion ports including a FeatherWing socket, it’s practically a handheld programming lab.
Then last week JP had Adalogger FeatherWing with its built‑in RTC on as his product pick of the week the idea clicked: let’s turn the PyBadge into a portable, multi‑function timing tool!
Components
Assemble the parts
Putting it all together is quite simple. The only soldering needed is attaching header pins to the AdaLogger.
- Solder header pins to the FeatherWing. Install the headers on the bottom of the board facing downward.
- Insert the coin cell into the FeatherWing.
- Attach the FeatherWing to the socket on the back of the PyBadge.
- Plug in the speaker
- Plug in the battery
- Connect to USB and you are ready to roll!
Install CircuitPython
Now install CircuitPython on your PyBadge - follow the instructions in the Learn Guide:
Install Libraries & Peripheral Files
You will also need to install a few libraries on your PyBadge - again this process is covered in the Learn Guide. The specific libraries needed for this project are:
- adfruit_pybadger (main helper library)
- adafruit_pcf8523 (for using the RTC on the FeatherWing)
- adafruit_bitmap_font
- adafruit_display_text
- adafruit_display_shapes
In addition to these libraries, you will also need several font files and place them in a folder "/fonts". Here is a good Learn Guide on font files for CircuitPython: Overview | Custom Fonts for CircuitPython Displays | Adafruit Learning System.
You will also need some wav files for the various sound effects if you want to add sound - these go in a folder "/wav".
On to coding
The original concept was just a simple stopwatch, but that quickly morphed in the final project which includes:
- Stopwatch Mode - timing to tenths of a second with lap capability
- Timer Mode - counting in seconds including noting the original start time
- Countdown Timer Mode - set the number of minutes/seconds and count down to zero
- Clock Mode - Display the time and date
- Battery voltage monitoring in the lower right corner
- Each screen shows the current time in the upper right corner
Here is what each mode looks like:
Basic Program Structure & Concepts
This project is useful for learning to use different elements to put together a complete project. Here are code snippets for the overall structure of the project. The complete code is posted at the end of the article.
Setting up the display: The PyBadge has the on-board display code built into the CP build, so it is quite simple: display = board.DISPLAY The PyBadge's display is a 160x128 color TFT. We will use displayio, display_text, display_shapes and bitmap_font to layout the display.
Set up the RTC: First you need to define the I2C bus and then set up the RTC:
i2c = board.I2C()rtc_pcf = PCF8523(i2c)
You need "one time" code to set the current on the RTC. Once set it should maintain the time as long as there is a coin cell battery installed. Time setting code:
if False: # change to True if you want to set the time!
# year, mon, date, hour, min, sec, wday, yday, isdst
t = time.struct_time((2025, 12, 19, 10, 45, 0, 4, -1, -1))
# you must set year, mon, date, hour, min, sec and weekday
# day of the week starts with 0 = Monday through 6 = Sunday
# yearday is not supported, isdst can be set but we don't do anything with it at this time
print("Setting time to:", t) # uncomment for debugging
rtc.datetime = t
To set the time on the first run change the False to True in the first line and update thet = time.struct_time line to have the current time starting from year to weekday. The last two entries can stay at -1. Typically I write this in a short file first and run it before I do all of the complex coding in a project. I have included a separate, short time setting program at the end of this article that I use when setting up my RTCs.
To read time in your RTC use the code:
t = rtc_pcf.datetimeprint(f"The date is {t.tm_mon}/{t.tm_mday}/{t.tm_year}")print(f"The time is {t.tm_hour}:{t.tm_min:02}:{t.tm_sec:02}")
You can learn all about the PFC8523 RTC in this Learn Guide: Adafruit PCF8523 Real Time Clock
Set up SD card: Although not really used for much in the project, since the FeatherWing has an SC card slot let's set this up as well. Set up the SPI and then the SC card:
spi = board.SPI()
cs = board.D10try:
sdcard = sdcardio.SDCard(spi, cs)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")
except Exception as e:
print(f"SD card not available: {e}")
It is useful to use try/except blocks in case there is a failure mounting the SD card the rest of the program can still run.
PyBadger Helper Library
Many functions are accessed using the adafruit_pybadger library. At the top of the program we import this library: from adafruit_pybadger import pybadger
The following components all use this library.
Set up the Neopixels:
We use the pybadger helper library use the five Neopixels.
The to use the Neopixels set the brightness and set the fill to 0 (off):
pybadger.pixels.brightness = 0.1
pybadger.pixels.fill(0)
If you wanted to fill them all with red:
pybadger.pixels.fill(255,0,0)
You can also address them individually using their index (0-4). To turn the middle pixel green use:
pybadger.pixels[2] = (0,255,0)
Read the light sensor:
Again, not currently used in this project, but it is very simple to read the value of the light sensor:
print(pybadger.light)
Prints the current value of the light sensor.
Read the battery voltage value:
There is a voltage divided connected to pin A6. Set this up as an analog in and calculate the voltage:
battery_pin = analogio.AnalogIn(board.A6)
raw_value = battery_pin.value
voltage = (raw_value / 65535.0) * 3.3
battery_voltage = voltage * 2
Using the PyBadge buttons:
The PyBadge has eight buttons that can use used in your project. These are also read using the pybadger helper library. They can be read using pybadger.button.xx where xx is:
- select
- start
- a
- b
- up
- down
- left
- right
Playing sounds - wav files:
The project uses several wav files for different sound effects - essentially beeps and blips to give feedback. Playing a wav file with the helper library is simple:
pybadger.play_file("/wav/xx.wav")
Playing tones:
The helper library also makes it simple to play tones - an alternate to playing a wav file. In this project I play a series of tones when the countdown timer reaches zero. To play a tone:
pybadger.play_tone(f, d)
Where 'f' is the frequency of the tone (i.e. 440 for "A") and 'd' is the duration in seconds.
Putting it all together
Now that the concept is defined and the core CircuitPython elements are in place, the real work begins. The project uses six different text areas across multiple screens, each updated dynamically depending on the active mode. Several font sizes and styles help make the most of the PyBadge’s limited screen space, keeping everything readable and visually appealing. From there, the challenge becomes managing the logic for each mode and its sub‑functions, all while using non‑blocking timing loops to keep the interface responsive.
If you’ve seen my other Playground projects, you know I use VS Code along with a Copilot Pro subscription. Having access to multiple AI agents has become an essential part of my workflow, and this project was no exception — much of the scaffolding and iterative refinement came from AI assistance.
Using AI to develop CircuitPython projects has been a huge time‑saver for me, but it still requires a solid understanding of the program’s intent and flow. Clear prompting is key: I make a point to reference specific text areas, variable names, and behaviors to avoid ambiguity. And while AI can handle about 90% of the coding, it occasionally makes illogical choices, so I often step in to adjust and fine‑tune the generated code to ensure everything works as intended.
Build an Enclosure
To really make this a finished project I designed and 3D printed a case for the PyBadge. Although there are a number of designs out in the world, I didn't find exactly what I was looking for. I find that Tinkercad is a great, fairly simple to use free resource for designing 3D prints. Although not as feature rich or robust as more common design software (think Fusion 360, FreeCAD, Blender, SolidWorks), with a little inspiration and innovation you can accomplish a lot with combining the various standard shapes in Tinkercad.
For this project I designed a lid with cutouts to match the PyBadge buttons and screen and openings for the USB, switch and ports. This lid is then friction fit into a matching cutout in the box. A couple dabs of hot glue keeps it all together.
Code
Time setting code
Name this code 'code.py' and copy to your board. Once the RTC clock is set you can replace this with your full project code.
# Simple code.py file to set the initial time on a PCF8523 RTC
# Change the if False to if True to set the time once, then change it back to False
# to run normally after that.
import time
import board
from adafruit_pcf8523.pcf8523 import PCF8523
days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
i2c = board.I2C()
rtc_pcf = PCF8523(i2c)
if False: # change to True if you want to set the time!
# year, mon, date, hour, min, sec, wday, yday, isdst
t = time.struct_time((2025, 12, 19, 9, 48, 0, 4, -1, -1))
# you must set year, mon, date, hour, min, sec and weekday
# yearday is not supported, isdst can be set but we don't do anything with it at this time
print("Setting time to:", t)
rtc.datetime = t
print()
print("Current time:", rtc_pcf.datetime)
while True:
now = rtc_pcf.datetime
print(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec
)
)
time.sleep(1)
import board
from adafruit_pybadger import pybadger
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect
from adafruit_pcf8523.pcf8523 import PCF8523
import sdcardio
import storage
import time
import displayio
import gc
import analogio
# Constants
MIN_COUNTDOWN = 10 # Minimum countdown time in seconds
MAX_COUNTDOWN = 5940 # Maximum countdown time in seconds (99 minutes)
DEFAULT_COUNTDOWN = 600 # Default countdown time in seconds (5 minutes)
NUM_NEOPIXELS = 5 # Number of NeoPixels on the PyBadge
STOPWATCH_COLOR_INTERVAL = 10 # Change color every 10 seconds in stopwatch mode
# Countdown color progression thresholds
COUNTDOWN_TWO_THIRDS = 0.67 # Change to yellow at 2/3 time remaining
COUNTDOWN_ONE_THIRD = 0.33 # Change to magenta at 1/3 time remaining
# Color constants
RESET_COLOR = (255, 0, 0) # Red color for reset state
COLOR_OFF = (0, 0, 0) # Off/Black
COLOR_GREEN = (0, 255, 0) # Green
COLOR_BLUE = (0, 0, 255) # Blue
COLOR_YELLOW = (255, 255, 0) # Yellow
COLOR_MAGENTA = (255, 0, 255) # Magenta
# Mode names
MODE_NAMES = ('Stopwatch', 'Countdown', 'Timer', 'Clock')
def convert_to_12hour(hour):
"""Convert 24-hour time to 12-hour format with AM/PM"""
if hour == 0:
return 12, "AM"
elif hour < 12:
return hour, "AM"
elif hour == 12:
return 12, "PM"
else:
return hour - 12, "PM"
def play_song():
for i in range(len(tns)):
print("Playing tone:", tns[i])
pybadger.play_tone(tns[i], 0.1)
def adjust_countdown_time(amount):
global countdown_duration
pybadger.play_file(wvs[1])
countdown_duration += amount
# Clamp between minimum and maximum
if countdown_duration < MIN_COUNTDOWN:
countdown_duration = MIN_COUNTDOWN
elif countdown_duration > MAX_COUNTDOWN:
countdown_duration = MAX_COUNTDOWN
print(f"Countdown set to {countdown_duration} seconds")
def time_display():
t = rtc_pcf.datetime
hour_12, am_pm = convert_to_12hour(t.tm_hour)
return f"{hour_12}:{t.tm_min:02d} {am_pm}"
def rotate_neopixel(current_time, color, interval):
"""Rotate NeoPixel animation - non-blocking with configurable interval"""
global current_pixel, last_pixel_update
if current_time - last_pixel_update >= interval:
# Turn off current pixel
pybadger.pixels[current_pixel] = COLOR_OFF
# Move to next pixel
current_pixel += 1
if current_pixel > NUM_NEOPIXELS - 1:
current_pixel = 0
# Turn on new pixel
pybadger.pixels[current_pixel] = color
last_pixel_update = current_time
display = board.DISPLAY
i2c = board.I2C()
rtc_pcf = PCF8523(i2c)
spi = board.SPI()
cs = board.D10
# Try to mount SD card with error handling
try:
sdcard = sdcardio.SDCard(spi, cs)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")
sd_available = True
print("SD card mounted successfully")
except Exception as e:
sd_available = False
print(f"SD card not available: {e}")
t = rtc_pcf.datetime
# Log startup time to SD card if available
if sd_available:
try:
on_time = f"{t.tm_year}{t.tm_mon:02}{t.tm_mday:02}_{t.tm_hour:02}{t.tm_min:02}{t.tm_sec:02}"
print(on_time)
with open("/sd/starts.csv", "a") as f:
f.write(on_time + "\r\n")
except Exception as e:
print(f"Failed to write to SD card: {e}")
print(f"The date is {t.tm_mon}/{t.tm_mday}/{t.tm_year}")
print(f"The time is {t.tm_hour}:{t.tm_min:02}:{t.tm_sec:02}")
pybadger.pixels.brightness = 0.1
pybadger.pixels.fill(0)
print(pybadger.light)
wvs = [ # This is a list of wav files available - to use a file refer to the list index i.e. wvs[0] for the first file
"/wav/click3.wav",
"/wav/pop2.wav",
"/wav/blip2.wav",
"/wav/snare_01.wav",
"/wav/beep.wav",
"/wav/confirmation_001.wav",
"/wav/confirmation_002.wav",
"/wav/confirmation_003.wav",
"/wav/confirmation_004.wav",
"/wav/threeTone1.wav",
"/wav/threeTone2.wav",
"/wav/twoTone1.wav",
"/wav/twoTone2.wav",
"/wav/crash_01.wav",
"/wav/chimes.wav",
"/wav/wand.wav",
]
tns = [ # This is a little ditty of notes to play when the countdown timer expires - know the tune?
659.3, 622.3, 659.3, 622.3, 659.3,
493.9, 587.3, 523.3, 440.0, 440, 440, 440, 440, 0, 0, 0, 494, 494, 494, 494, 494, 0, 0, 0, 523, 523, 523, 523, 523
]
font8 = bitmap_font.load_font("/fonts/Calibri-12.pcf")
font15 = bitmap_font.load_font("/fonts/Calibri-15.pcf")
font12 = bitmap_font.load_font("/fonts/ComicSansMS-10.pcf")
font38 = bitmap_font.load_font("/fonts/Calibri-38.pcf")
main_group = displayio.Group()
background = Rect(0, 0, display.width, display.height, fill=0x000088)
# Display layout positions
LABEL_X_OFFSET = 5
TITLE_Y_POS = 5
MAIN_DISPLAY_Y_POS = 25
STATUS_Y_POS = 65
INFO_Y_POS = 100
bv_label = label.Label(font12, anchored_position=(display.width - 5, display.height - 5), anchor_point=(1.0, 1.0), text="", color=0x9999FF)
line_1 = label.Label(font15, anchored_position=(LABEL_X_OFFSET, TITLE_Y_POS), anchor_point=(0.0, 0.0), text="", color=0xFFAA33)
line_1a = label.Label(font38, anchored_position=(80, MAIN_DISPLAY_Y_POS), anchor_point=(0.5, 0.0), text="", color=0xFFFF00)
line_2 = label.Label(font15, anchored_position=(LABEL_X_OFFSET, STATUS_Y_POS), anchor_point=(0.0, 0.0), text="", color=0xFFFFFF)
line_3 = label.Label(font12, anchored_position=(LABEL_X_OFFSET, INFO_Y_POS), anchor_point=(0.0, 0.0), text="", color=0x00FF00)
line_clk = label.Label(font8, anchored_position=(display.width - 3, 3), anchor_point=(1.0, 0.0), text="", color=0xFFFFFF)
main_group.append(background)
main_group.append(line_1)
main_group.append(line_1a)
main_group.append(line_2)
main_group.append(line_3)
main_group.append(bv_label)
main_group.append(line_clk)
display.root_group = main_group
# Mode: 0 = Stopwatch, 1 = Countdown, 2 = Clock, 3 = Timer
mode = 0
# Stopwatch variables
stopwatch_running = False
stopwatch_start_time = 0
stopwatch_elapsed = 0
stopwatch_lap_mode = False # True when displaying lap time
stopwatch_lap_time = 0 # The frozen lap time to display
# Timer variables
timer_running = False
timer_start_time = 0
timer_elapsed = 0
timer_clock_start_hour = 0
timer_clock_start_min = 0
# Countdown variables
countdown_running = False
countdown_paused = False
countdown_start_time = 0
countdown_pause_elapsed = 0
countdown_duration = DEFAULT_COUNTDOWN
countdown_remaining = 0
last_minute_notified = -1 # Track last minute we played sound for
# Button state tracking for debouncing
start_pressed = False
select_pressed = False
a_pressed = False
b_pressed = False
up_pressed = False
down_pressed = False
left_pressed = False
right_pressed = False
# Color cycling for NeoPixels
colors = [(0, 255, 0), (255, 0, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255)]
# Green, Red, Blue, Yellow, Magenta, Cyan
color_index = 0
# Display update tracking - only update when values change
prev_stopwatch_time = ""
prev_stopwatch_status = ""
prev_countdown_time = ""
prev_countdown_status = ""
prev_countdown_set = ""
prev_mode = -1 # Track mode changes
# NeoPixel animation tracking
current_pixel = 0
pcolor = colors[0]
last_pixel_update = 0 # Timestamp of last NeoPixel update
PIXEL_UPDATE_INTERVAL = 1.0 # Update NeoPixels every 1 second in countdown mode
# Memory monitoring
last_gc_time = 0
GC_INTERVAL = 5.0 # Run garbage collection every 5 seconds
last_mem_update = 0
MEM_UPDATE_INTERVAL = 2.0 # Update memory display every 2 seconds
# Clock display monitoring
last_clk_update = 0
CLK_UPDATE_INTERVAL = 30.0 # Update clock display every 30 seconds
# Battery voltage monitoring
battery_voltage_cached = 0.0
last_battery_update = 0
BATTERY_UPDATE_INTERVAL = 10.0 # Update battery voltage every 10 seconds
# Clock mode optimization
prev_clock_minute = -1 # Track last displayed minute in clock mode
# Initial garbage collection
gc.collect()
print(f"Initial free memory: {gc.mem_free()} bytes")
# Set up battery voltage monitoring on A6
battery_pin = analogio.AnalogIn(board.A6)
def get_battery_voltage():
"""Read the battery voltage from A6 (connected to voltage divider)"""
# Read the raw analog value (0-65535)
raw_value = battery_pin.value
# Convert to voltage (reference voltage is typically 3.3V)
voltage = (raw_value / 65535.0) * 3.3
# If using a voltage divider, multiply by the divider ratio
# For PyBadge, the divider is typically 2:1, so multiply by 2
battery_voltage = voltage * 2
return battery_voltage
# Initialize and print battery voltage
battery_voltage_cached = get_battery_voltage()
print(f"Battery voltage: {battery_voltage_cached:.2f}V")
# Loop forever so you can enjoy your text
while True:
# Get current time once at start of loop
current_time = time.monotonic()
# SELECT button - toggle between modes with debouncing
if pybadger.button.select:
bv_label.text = f"{battery_voltage_cached:.2f}v"
if not select_pressed:
select_pressed = True
# Only allow mode switch when nothing is running
if not stopwatch_running and not countdown_running and not timer_running:
pybadger.play_file(wvs[8])
mode = (mode + 1) % 4 # Cycle through 0, 1, 2, 3
print(f"Mode: {MODE_NAMES[mode]}")
else:
select_pressed = False
if mode == 0: # STOPWATCH MODE
# Update mode title if mode changed
if mode != prev_mode:
line_1.text = "STOPWATCH"
line_3.text = "START=Run/Stop - B for Lap\nSELECT=Mode A=Reset"
line_1a.text = "00:00.0"
line_2.text = "STOPPED"
prev_stopwatch_time = "00:00.0"
prev_stopwatch_status = "STOPPED"
prev_mode = mode
# START button - toggle start/stop
if pybadger.button.start:
if not start_pressed:
start_pressed = True
pybadger.play_file(wvs[2])
if not stopwatch_running:
stopwatch_running = True
stopwatch_start_time = time.monotonic() - stopwatch_elapsed
print("Stopwatch started")
else:
stopwatch_running = False
stopwatch_elapsed = time.monotonic() - stopwatch_start_time
print("Stopwatch stopped")
else:
start_pressed = False
# A button - reset
if pybadger.button.a:
if not a_pressed and not stopwatch_running: # Only allow reset when stopped
a_pressed = True
pybadger.play_file(wvs[9])
stopwatch_running = False
stopwatch_elapsed = 0
stopwatch_start_time = 0
stopwatch_lap_mode = False
stopwatch_lap_time = 0
print("Stopwatch reset")
pcolor = RESET_COLOR
else:
a_pressed = False
# B button - lap timing (only when running)
if pybadger.button.b:
if not b_pressed and stopwatch_running:
b_pressed = True
pybadger.play_file(wvs[7])
if not stopwatch_lap_mode:
# Freeze display at current time
stopwatch_lap_time = time.monotonic() - stopwatch_start_time
stopwatch_lap_mode = True
print(f"Lap time: {stopwatch_lap_time:.1f}s")
else:
# Resume showing current time
stopwatch_lap_mode = False
print("Lap resumed")
else:
b_pressed = False
# Update elapsed time if running
if stopwatch_running:
stopwatch_elapsed = time.monotonic() - stopwatch_start_time
# Change color based on interval
color_index = int(stopwatch_elapsed // STOPWATCH_COLOR_INTERVAL) % len(colors)
pcolor = colors[color_index]
# Display stopwatch time
# Use lap time if in lap mode, otherwise use current elapsed time
display_time = stopwatch_lap_time if stopwatch_lap_mode else stopwatch_elapsed
tenths = int(display_time * 10)
seconds = tenths // 10
remaining_tenths = tenths % 10
minutes = seconds // 60
remaining_seconds = seconds % 60
# Only update display if time changed
display_time_str = f"{minutes:02d}:{remaining_seconds:02d}.{remaining_tenths}"
if display_time_str != prev_stopwatch_time:
line_1a.text = display_time_str
prev_stopwatch_time = display_time_str
# Only update status if changed
if stopwatch_lap_mode:
current_status = "LAP - B to RESUME"
else:
current_status = "RUNNING" if stopwatch_running else "STOPPED"
if current_status != prev_stopwatch_status:
line_2.text = current_status
prev_stopwatch_status = current_status
# Animate NeoPixels while running (non-blocking)
if stopwatch_running:
rotate_neopixel(current_time, pcolor, 0.1)
# Small sleep to prevent tight looping
time.sleep(0.01)
elif mode == 1: # COUNTDOWN MODE
# Update mode title if mode changed
if mode != prev_mode:
line_1.text = "COUNTDOWN"
# Display countdown in MM:SS format
total_seconds = int(countdown_duration)
minutes = total_seconds // 60
seconds = total_seconds % 60
line_1a.text = f"{minutes:02d}:{seconds:02d}"
line_2.text = "STOPPED\nUP/DOWN to set time"
prev_countdown_time = f"{minutes:02d}:{seconds:02d}"
prev_countdown_status = "STOPPED\nUP/DOWN to set time"
prev_mode = mode
# UP button - increase countdown time (when not running)
if pybadger.button.up:
if not up_pressed and not countdown_running:
up_pressed = True
adjust_countdown_time(60) # Add 1 minute
else:
up_pressed = False
# DOWN button - decrease countdown time (when not running)
if pybadger.button.down:
if not down_pressed and not countdown_running:
down_pressed = True
adjust_countdown_time(-60) # Subtract 1 minute
else:
down_pressed = False
# RIGHT button - increase countdown time by 10 seconds (when not running)
if pybadger.button.right:
if not right_pressed and not countdown_running:
right_pressed = True
adjust_countdown_time(10) # Add 10 seconds
else:
right_pressed = False
# LEFT button - decrease countdown time by 10 seconds (when not running)
if pybadger.button.left:
if not left_pressed and not countdown_running:
left_pressed = True
adjust_countdown_time(-10) # Subtract 10 seconds
else:
left_pressed = False
# B button - reset countdown time to default (when not running)
if pybadger.button.b:
if not b_pressed and not countdown_running:
b_pressed = True
pybadger.play_file(wvs[4])
countdown_duration = DEFAULT_COUNTDOWN
print(f"Countdown reset to {DEFAULT_COUNTDOWN // 60}:00")
else:
b_pressed = False
# START button - toggle start/pause countdown
if pybadger.button.start:
if not start_pressed:
start_pressed = True
pybadger.play_file(wvs[5])
if not countdown_running:
# Start or resume countdown
countdown_running = True
countdown_paused = False
countdown_start_time = time.monotonic() - countdown_pause_elapsed
print("Countdown started")
elif countdown_paused:
# Resume from pause
countdown_paused = False
countdown_start_time = time.monotonic() - countdown_pause_elapsed
print("Countdown resumed")
else:
# Pause the countdown
countdown_paused = True
countdown_pause_elapsed = time.monotonic() - countdown_start_time
print("Countdown paused")
else:
start_pressed = False
# A button - reset countdown (only when stopped or paused)
if pybadger.button.a and (not countdown_running or countdown_paused):
if not a_pressed:
a_pressed = True
pybadger.play_file(wvs[6])
countdown_running = False
countdown_paused = False
countdown_start_time = 0
countdown_pause_elapsed = 0
last_minute_notified = -1
print("Countdown reset")
pcolor = RESET_COLOR
else:
a_pressed = False
# Update countdown
if countdown_running and not countdown_paused:
elapsed = time.monotonic() - countdown_start_time
countdown_remaining = countdown_duration - elapsed
# Check for even minute milestones
minutes_remaining = int(countdown_remaining // 60)
if minutes_remaining > 0 and minutes_remaining % 2 == 0 and minutes_remaining != last_minute_notified:
pybadger.play_file(wvs[12])
last_minute_notified = minutes_remaining
print(f"Even minute reached: {minutes_remaining}")
if countdown_remaining <= 0:
countdown_remaining = 0
countdown_running = False
countdown_paused = False
countdown_pause_elapsed = 0
print("Countdown finished!")
# Play song 3 times with 2 second pauses, allowing cancellation
for i in range(3):
play_song()
if i < 2: # Don't pause after the last song
# Check for button press to cancel alarm
cancel_start = time.monotonic()
while time.monotonic() - cancel_start < 2:
if pybadger.button.a or pybadger.button.start or pybadger.button.select:
print("Alarm cancelled")
break
time.sleep(0.05)
else:
continue
# If we broke out of while loop, break out of for loop too
break
pcolor = RESET_COLOR # Red when done
else:
# Color based on time remaining
if countdown_remaining <= 60: # Last minute
pcolor = RESET_COLOR # Red
elif countdown_remaining <= countdown_duration * COUNTDOWN_ONE_THIRD:
pcolor = COLOR_MAGENTA
elif countdown_remaining <= countdown_duration * COUNTDOWN_TWO_THIRDS:
pcolor = COLOR_YELLOW
else:
pcolor = COLOR_GREEN
elif countdown_paused:
# When paused, set color to blue and preserve remaining time
pcolor = COLOR_BLUE
countdown_remaining = countdown_duration - countdown_pause_elapsed
else:
countdown_remaining = countdown_duration
# Display countdown time
total_seconds = int(countdown_remaining)
minutes = total_seconds // 60
seconds = total_seconds % 60
# Only update display if time changed
display_time_str = f"{minutes:02d}:{seconds:02d}"
if display_time_str != prev_countdown_time:
line_1a.text = display_time_str
prev_countdown_time = display_time_str
# Only update status if changed
if countdown_paused:
current_status = "PAUSED"
elif countdown_running:
current_status = "RUNNING"
else:
current_status = "STOPPED\nUP/DOWN to set time"
if current_status != prev_countdown_status:
line_2.text = current_status
prev_countdown_status = current_status
# Only update set time if changed
set_minutes = countdown_duration // 60
set_seconds = countdown_duration % 60
current_set = f"Set: {set_minutes:02d}:{set_seconds:02d}"
if current_set != prev_countdown_set:
line_3.text = current_set
prev_countdown_set = current_set
# Animate NeoPixels while running (non-blocking, updates every second)
if countdown_running and not countdown_paused:
rotate_neopixel(current_time, pcolor, PIXEL_UPDATE_INTERVAL)
elif countdown_paused:
# Keep current pixel lit blue when paused
if current_pixel > NUM_NEOPIXELS - 1:
current_pixel = NUM_NEOPIXELS - 1
pybadger.pixels[current_pixel] = pcolor # Light up blue
# Small sleep to prevent tight looping
time.sleep(0.01)
elif mode == 2: # TIMER MODE
# Update mode title if mode changed
if mode != prev_mode:
line_1.text = "TIMER"
line_3.text = "START=Run/Stop A=Reset\nSELECT=Mode"
line_1a.text = "00:00"
line_2.text = "Ready"
prev_mode = mode
# START button - toggle start/stop
if pybadger.button.start:
if not start_pressed:
start_pressed = True
pybadger.play_file(wvs[2])
if not timer_running:
timer_running = True
timer_start_time = time.monotonic() - timer_elapsed
# Capture the current clock time only when starting from zero
if timer_elapsed == 0:
t = rtc_pcf.datetime
timer_clock_start_hour = t.tm_hour
timer_clock_start_min = t.tm_min
print(f"Timer started at {timer_clock_start_hour}:{timer_clock_start_min:02d}")
else:
print("Timer resumed")
else:
timer_running = False
timer_elapsed = time.monotonic() - timer_start_time
print("Timer stopped")
else:
start_pressed = False
# A button - reset
if pybadger.button.a:
if not a_pressed and not timer_running: # Only allow reset when stopped
a_pressed = True
pybadger.play_file(wvs[9])
timer_running = False
timer_elapsed = 0
timer_start_time = 0
timer_clock_start_hour = 0
timer_clock_start_min = 0
print("Timer reset")
pcolor = RESET_COLOR
else:
a_pressed = False
# Update elapsed time if running
if timer_running:
timer_elapsed = time.monotonic() - timer_start_time
# Change color based on time
color_index = int(timer_elapsed // 10) % len(colors)
pcolor = colors[color_index]
# Display timer time
total_seconds = int(timer_elapsed)
hours = total_seconds // 3600
remaining_seconds = total_seconds % 3600
minutes = remaining_seconds // 60
seconds = remaining_seconds % 60
# Display format changes based on elapsed time
if hours > 0 or total_seconds >= 3600: # Show hours:minutes after 60 minutes
timer_display = f"{hours:02d}:{minutes:02d}"
else: # Show minutes:seconds
timer_display = f"{minutes:02d}:{seconds:02d}"
line_1a.text = timer_display
# Display start time instead of RUNNING/STOPPED
if timer_running or timer_elapsed > 0:
# Convert to 12-hour format
hour_12, am_pm = convert_to_12hour(timer_clock_start_hour)
start_time_str = f"Started: {hour_12}:{timer_clock_start_min:02d} {am_pm}"
line_2.text = start_time_str
else:
line_2.text = "Ready"
# Animate NeoPixels while running (rotate every second)
if timer_running:
rotate_neopixel(current_time, pcolor, 1.0)
else:
time.sleep(0.01)
elif mode == 3: # CLOCK MODE
# Update mode title if mode changed
if mode != prev_mode:
line_1.text = "CLOCK"
line_3.text = "SELECT=Mode"
prev_mode = mode
prev_clock_minute = -1 # Force update on mode entry
# Get current time from RTC
t = rtc_pcf.datetime
# Only update display when minute changes
if t.tm_min != prev_clock_minute:
# Convert to 12-hour format with AM/PM
hour_12, am_pm = convert_to_12hour(t.tm_hour)
# Format time string
time_str = f"{hour_12}:{t.tm_min:02d} {am_pm}"
line_1a.text = time_str
# Format date string
date_str = f"{t.tm_mon}/{t.tm_mday}/{t.tm_year}"
line_2.text = date_str
prev_clock_minute = t.tm_min
# Set NeoPixels to off
pcolor = COLOR_OFF
pybadger.pixels.fill(pcolor)
# Small sleep
time.sleep(0.5)
# Periodic garbage collection
if current_time - last_gc_time >= GC_INTERVAL:
gc.collect()
last_gc_time = current_time
# Periodic memory monitoring
if current_time - last_mem_update >= MEM_UPDATE_INTERVAL:
mem_free_kb = gc.mem_free() // 1024
last_mem_update = current_time
# Update clock display
if current_time - last_clk_update >= CLK_UPDATE_INTERVAL:
line_clk.text = time_display()
last_clk_update = current_time
# Update battery voltage cache
if current_time - last_battery_update >= BATTERY_UPDATE_INTERVAL:
battery_voltage_cached = get_battery_voltage()
last_battery_update = current_time
This page (PyBadge Timer Box) was last updated on December 19, 2025.
Text editor powered by tinymce.