timer-bar/colorbar-fastloop.py

350 lines
11 KiB
Python

import time
import board
import neopixel
import adafruit_datetime as datetime
from adafruit_seesaw import seesaw, rotaryio, digitalio
from adafruit_seesaw import neopixel as seesaw_neopixel
from adafruit_debouncer import Button
import busio
import displayio
import terminalio
import adafruit_displayio_ssd1306
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
# Release displays so thonny doesn't block the i2c setup
displayio.release_displays()
####
# i2c bus setup
#SDA = board.GP0
#SCL = board.GP1
#i2c = busio.I2C(SCL, SDA)
i2c = board.STEMMA_I2C()
# END i2c bus setup
####
####
# Rotary Encoder setup
seesaw = seesaw.Seesaw(i2c, 0x36)
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
pin = digitalio.DigitalIO(seesaw, 24)
button = Button(pin)
encoder = rotaryio.IncrementalEncoder(seesaw)
last_position = -1
# END Rotary Encoder setup
####
####
# Display setup
try:
display_bus = displayio.I2CDisplay(i2c, device_address=60)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=32)
except:
print("Unable to initialize display.")
text = "HELLO WORLD"
font = bitmap_font.load_font("/LiberationMono-Bold-42.pcf")
color = 0xFFFFFF
text_area = label.Label(font, text=text, color=color)
text_area.x = 1
text_area.y = 12
try:
display.show(text_area)
except:
print("Unable to print to display.")
pass
# END Display setup
####
####
# Neopixel setup
# Set Constants
pixel_pin = board.A3
num_pixels = 30
brightness = 0.2
# Create neopixel object named pixels
pixels = neopixel.NeoPixel(
pixel_pin,
num_pixels,
brightness=brightness,
auto_write=False,
pixel_order="GRB"
)
# Set up user-facing neopixel on rotary breakout
userpixel = seesaw_neopixel.NeoPixel(seesaw, 6, 1,
brightness=brightness,
auto_write=False)
# Colors used in this script
# FORMAT: (R, G, B, W)
RED = (255, 0, 0, 0)
YELLOW = (255, 150, 0, 0)
GREEN = (0, 255, 0, 0)
BLANK = (0, 0, 0, 0)
# Turn all pixels off
pixels.fill(BLANK)
pixels.show()
userpixel.fill(BLANK)
userpixel.show()
# END Neopixel setup
####
# Use datetime.timedelta to convert an int of seconds to a
# string with the format MM:SS
def prettytime(seconds):
return str(datetime.timedelta(seconds=abs(seconds)))[2:]
# Set the color on a single neopixel based on colormode and
# whether yellowtime or redtime has been reached. Calling
# logic should iterate over every neopixel ID that should
# be updated during the current update interval, and then
# call pixels.show() after all pixels have been set. It's
# up to the calling logic to calculate whether the yellow
# or red parameters should be set to True. Behavior when
# red and yellow are both set to True depends on how the
# colormode is configured.
def colorizer(pxnum, colormode="fill", yellow=False, red=False):
# Fill every pixel from lowest to currently highest with
# the current color.
if colormode == "fill":
if red:
pixels[pxnum] = RED
elif yellow:
pixels[pxnum] = YELLOW
else:
pixels[pxnum] = GREEN
# Only fill the next pixel with the current color if it's
# currently BLANK
elif colormode == "candybar":
if pixels[pxnum] == BLANK:
if red:
pixels[pxnum] = RED
elif yellow:
pixels[pxnum] = YELLOW
else:
pixels[pxnum] = GREEN
else:
pass
else:
# Invalid colormodes end up here
raise Exception("Invalid colormode: " + colormode)
# Pause the timer until the user resumes it again
def pause():
# We are paused
pause = True
print("Timer Paused")
# Keep looping here as long as we're paused
while pause:
# Update the debouncer
button.update()
# Resume timer on single short button press
if button.short_count == 1:
pause = False
print("Timer Resumed")
# Reset timer on long press
if button.long_press:
pause = False
print("Timer Reset")
# When resetting the timer, return with a specific
# value so that the calling logic can handle this case.
return "reset"
# If we've exited the loop here, then resume
# the timer by simply returning
return
# Count down from the given total seconds, using the chosen
# colormode (how the colors are filled into each pixel),
# and the given yellowtime (seconds before timer has elapsed
# that the bar should show yellow), and redtime (same as
# yellowtime). The colormode determines what happens at
# yellowtime and redtime.
def countdown(
seconds,
colormode="fill",
yellowtime=120,
redtime=60,
update_interval=1):
# Turn all pixels off
pixels.fill(BLANK)
pixels.show()
userpixel.fill(BLANK)
userpixel.show()
# Init the update interval tracking variable
last_update_time = -1
# Init the current time variable
current_time = seconds
# This begins what I like to call the "Are We There Yet?"
# loop. Instead of making the script wait for an interval
# before continuing as a form of forced timed pacing, we
# simply write an infinite loop that will iterate very
# quickly between update intervals, essentially repeatedly
# asking the CPU to calculate whether it's time to do an
# update yet. The high frequency of the update interval
# checks will make sure our update is fired on-time.
#
# ...unless we decide to configure a way to kill the loop entirely, I guess...? ;)
while True:
# Get the current time
now = time.monotonic()
# Is it time for an update yet?
if now >= last_update_time + update_interval:
# Update the last update time
last_update_time = now
# Do update stuff
# Calculate the current position.
# Takes the percentage of time elapsed, multiplied with
# the total numbers of pixels, and rounded to the nearest
# decimal. This results in a number of pixels proportional
# to the elapsed time
try:
current_position = round(num_pixels * ((seconds - current_time) / seconds))
except ZeroDivisionError:
# If the previous line was deviding by zero, then just
# call for *all* pixels to be lit
current_position = num_pixels
# Catch a couple of special cases
if current_position == 0:
# Light the first LED when the timer starts
# regardless of other factors
colorizer(0, colormode)
userpixel.fill(GREEN)
elif current_position == num_pixels and current_time > 0:
# If current_position calls for *all*
# pixels to be lit, and the timer
# hasn't expired yet, don't do anything.
# This will delay the last pixel from
# lighting until the timer has fully elapsed
pass
else:
# Loop over every pixel ID that should be lit
# based on the elapsed time
for pixel in range(current_position):
# Set pixel color stuff
# If current_time has gone negative, don't
# change any pixels, just keep counting for
# user feedback
if current_time < 0:
pass
elif current_time <= redtime:
colorizer(pixel, colormode, red=True)
userpixel.fill(RED)
elif current_time <= yellowtime:
colorizer(pixel, colormode, yellow=True)
userpixel.fill(YELLOW)
else:
colorizer(pixel, colormode)
userpixel.fill(GREEN)
# All the pixels have now been set based on the
# specified colormode, now display the result IRL.
pixels.show()
userpixel.show()
# Increment the elapsed time variable
current_time -= update_interval
# Give the user feedback
text_area.text = prettytime(current_time)
print("current time: " + prettytime(current_time))
# Update the debouncer
button.update()
# Pause on single short button press
if button.short_count == 1:
if pause() == "reset":
return
# Hard-coded initial value. (will replace with stored value later)
set_time_orig = 120
set_time = set_time_orig
# How many seconds should be added to or subtracted from set_time
# for every encoder click
set_time_step = 60
# Main loop
while True:
# Update the debouncer
button.update()
# Negate the position to make clockwise rotation positive
position = -encoder.position
# If the encoder position has changed since last iteration
if position != last_position:
# If last_position is set to -1, assume it's just been
# initialized, so don't adjust anything
if last_position == -1:
pass
# Clockwise turn increases set_time by set_time_step
elif position > last_position:
set_time += set_time_step
# Counter-clockwise turn decreases set_time by set_time_step
# only until 0
elif set_time > 1:
set_time -= set_time_step
# Update the position tracker
last_position = position
# User feedback
text_area.text = prettytime(set_time)
print("Current set time: " + prettytime(set_time))
# Reset timer to config value on long press
if button.long_press:
# Reset the set_time to the value of set_time_orig
# (eventually, set_time_orig will be read from persistent config)
set_time = set_time_orig
# Give the user feedback
text_area.text = prettytime(set_time)
print("Time reset to: " + prettytime(set_time))
# Start the timer on single short press
if button.short_count == 1:
# Start the countdown using the configured set_time
countdown(set_time)
# Once the timer has been reset, re-init last_position.
# In effect, this will display the set_time to the user again
last_position = -1
# Turn off all pixels
pixels.fill(BLANK)
pixels.show()
userpixel.fill(BLANK)
userpixel.show()