289 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			9.2 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
 | |
| 
 | |
| 
 | |
| ####
 | |
| # 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
 | |
| ####
 | |
| 
 | |
| 
 | |
| ####
 | |
| # Neopixel setup
 | |
| 
 | |
| # Set Constants
 | |
| pixel_pin = board.GP5
 | |
| 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, 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):
 | |
|     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)
 | |
| 
 | |
| 
 | |
| # 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
 | |
|             current_position = round(num_pixels * ((seconds - current_time) / seconds))
 | |
| 
 | |
|             # 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
 | |
| 
 | |
|             # 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 current_time < 0:
 | |
|                 display_time_sign = "-"
 | |
|             else:
 | |
|                 display_time_sign = " "
 | |
|             # Give the user feedback
 | |
|             #  (this string will eventually go to a ssd1306 OLED display via
 | |
|             #  displayio, but just put it on the terminal output for now)
 | |
|             print("current time: " + display_time_sign + prettytime(current_time))
 | |
| 
 | |
|         # Update the debouncer
 | |
|         button.update()
 | |
| 
 | |
|         # Pause on single short button press
 | |
|         if button.short_count == 1:
 | |
|             # 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")
 | |
|                     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 > 0:
 | |
|             set_time -= set_time_step
 | |
|         # Update the position tracker
 | |
|         last_position = position
 | |
|         # User feedback
 | |
|         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
 | |
|         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()
 |