339 lines
10 KiB
Python
339 lines
10 KiB
Python
import time
|
|
import board
|
|
import neopixel
|
|
import adafruit_datetime as datetime
|
|
from adafruit_seesaw import seesaw, rotaryio, digitalio
|
|
from adafruit_debouncer import Button
|
|
import busio
|
|
import displayio
|
|
import terminalio
|
|
import adafruit_displayio_ssd1306
|
|
from adafruit_display_text import label
|
|
|
|
# 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)
|
|
|
|
# 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 = terminalio.FONT
|
|
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.GP10
|
|
num_pixels = 144
|
|
brightness = 0.1
|
|
|
|
# Create neopixel object named pixels
|
|
pixels = neopixel.NeoPixel(
|
|
pixel_pin,
|
|
num_pixels,
|
|
brightness=brightness,
|
|
auto_write=False,
|
|
pixel_order="GRBW"
|
|
)
|
|
|
|
# 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)
|
|
|
|
# Turn all pixels off
|
|
pixels.fill(BLANK)
|
|
pixels.show()
|
|
|
|
# END Neopixel setup
|
|
####
|
|
|
|
|
|
# Use datetime.timedelta to convert an int of seconds to a
|
|
# string with the format MM:SS
|
|
def prettytime(seconds):
|
|
# Add a negative sign to the output when current_time is negative.
|
|
# prettytime() puts the given value through abs() because the way
|
|
# datetime.timedelta() represents negative values is kind of a PITA
|
|
# to deal with.
|
|
if seconds < 0:
|
|
display_time_sign = "-"
|
|
else:
|
|
display_time_sign = ""
|
|
|
|
return display_time_sign + 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()
|
|
|
|
# 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)
|
|
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)
|
|
elif current_time <= yellowtime:
|
|
colorizer(pixel, colormode, yellow=True)
|
|
else:
|
|
colorizer(pixel, colormode)
|
|
|
|
# All the pixels have now been set based on the
|
|
# specified colormode, now display the result IRL.
|
|
pixels.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()
|