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()