The QT Py Activity Timer is meant to time two separate activities, such as working and taking a break. Flip it to begin timing the first activity, such as working for 120 minutes. The timer will count down by fading the LEDs from one color to another, at which point it gives you a moment to flip it before blinking at you to let you know you haven't moved onto the next activity. Flip it to begin timing the next activity, such as a 15 minute break. The process repeats. You can customise the colors and timing to fit your aesthetics and needs. Flip it on its side to stop both timers and turn off the LEDs. Let's get timing!

Code

Save the following to your CIRCUITPY drive as code.py:

import time
import board
import adafruit_lis3dh
import neopixel

# Length of each activity, in minutes.
activity_one = 120
activity_two = 15

# Length of time, in minutes, before timer begins Red Alert blinking.
red_alert_delay = 2

# The color will fade from color_begin to color_complete. This is a gentle indicator of how much
# time has passed. Set to any two colors.
color_begin = (0, 255, 0)  # Green. Set to any color to begin your countdown.
color_complete = (255, 0, 0)  # Red. Set to any color to end your countdown.
led_brightness = 0.2  # Set to a number between 0.0 and 1.0 to set LED brightness.

# Increase or decrease this to change the speed of the Red Alert blinking in seconds.
red_alert_blink_speed = 0.5

# These are the thresholds the z-axis values must exceed for the orientation to be considered
# "up" or "down". These values are valid if the LIS3DH breakout is mounted flat inside the timer
# assembly. If you want the option to have the timer on an angle while timing, you can calibrate
# these values by uncommenting the print(z) in orientation() to see the z-axis values and update
# the thresholds to match the desired range.
down_threshold = -8
up_threshold = 8

# Number of LEDs. Default is 32 (2 x rings of 16 each). If you use a different form of NeoPixels,
# update this to match the total number of pixels.
number_of_pixels = 32

# Set up accelerometer and LEDs.
lis3dh = adafruit_lis3dh.LIS3DH_I2C(board.I2C())
pixels = neopixel.NeoPixel(board.A3, number_of_pixels, brightness=led_brightness)
pixels.fill(0)  # Turn off the LEDs if they're on.

STATE_IDLE = "Idle"
STATE_TIMING = "Timing"
STATE_TIMING_COMPLETE = "Timing complete"
RED_ALERT = "Red Alert"


def gradient(color_one, color_two, blend_weight):
    """
    Blend between two colors with a given ratio.

    :param color_one:  first color, as an (r,g,b) tuple
    :param color_two:  second color, as an (r,g,b) tuple
    :param blend_weight: Blend weight (ratio) of second color, 0.0 to 1.0
    """
    if blend_weight < 0.0:
        blend_weight = 0.0
    elif blend_weight > 1.0:
        blend_weight = 1.0
    initial_weight = 1.0 - blend_weight
    return (int(color_one[0] * initial_weight + color_two[0] * blend_weight),
            int(color_one[1] * initial_weight + color_two[1] * blend_weight),
            int(color_one[2] * initial_weight + color_two[2] * blend_weight))


# pylint: disable=global-statement
def set_state(state):
    global current_state, state_changed
    current_state = state
    state_changed = time.monotonic()
    print("Current state:", current_state)  # Print the current state.


def orientation():
    """Determines orientation based on z-axis values. Thresholds set above."""
    _, _, z = lis3dh.acceleration
    # print(z)  # Uncomment to print the z-axis value. Use to calibrate thresholds if desired.
    if z < down_threshold:
        return 'down'
    if z > up_threshold:
        return 'up'
    return 'side'


def orientation_debounced():
    """
    Debounces the orientation changes. For a new orientation to be registered, the timer must
    be in that orientation for the duration of the delay.
    """
    delay = 0.2
    initial_time = time.monotonic()
    initial_orientation = orientation()
    while True:
        new_orientation = orientation()
        if new_orientation != initial_orientation:
            initial_time = time.monotonic()
            initial_orientation = new_orientation
            continue
        if time.monotonic() - initial_time > delay:
            return new_orientation


def state_from_orientation():
    """Determines the state based on orientation."""
    global current_orientation
    new_orientation = orientation_debounced()
    if new_orientation != current_orientation:
        if new_orientation == 'side':
            set_state(STATE_IDLE)
            current_orientation = orientation_debounced()
            return
        set_state(STATE_TIMING)
        current_orientation = orientation_debounced()


def idle():
    """The idle state."""
    pixels.fill(0)
    state_from_orientation()


def timing():
    """The timing active state."""
    state_from_orientation()

    if current_orientation == 'up':
        activity_duration = activity_one * 60  # Converts seconds to minutes.
    elif current_orientation == 'down':
        activity_duration = activity_two * 60  # Converts seconds to minutes.
    else:
        return

    elapsed = time.monotonic() - state_changed

    if elapsed >= activity_duration:
        set_state(STATE_TIMING_COMPLETE)
        return

    blend = (elapsed / activity_duration)
    pixels.fill(gradient(color_begin, color_complete, blend))


def timing_complete():
    """The timing complete state."""
    pixels.fill(color_complete)

    state_from_orientation()

    elapsed = time.monotonic() - state_changed

    if elapsed >= (red_alert_delay * 60):  # Converts seconds to minutes.
        set_state(RED_ALERT)
        return


blink_is_on = False


def red_alert():
    """The Red Alert state."""
    global blink_is_on
    elapsed = time.monotonic() - state_changed
    if elapsed >= red_alert_blink_speed:
        set_state(RED_ALERT)
        blink_is_on = not blink_is_on
    if blink_is_on:
        pixels.fill(color_complete)
    else:
        pixels.fill(0)

    state_from_orientation()


current_orientation = orientation_debounced()  # Get the initial orientation.
state_changed = time.monotonic()  # Set an initial timestamp.
current_state = STATE_IDLE  # Set initial state to idle.

print("Start state:", current_state)  # Print the starting state.

while True:
    if current_state == STATE_IDLE:
        idle()
    if current_state == STATE_TIMING:
        timing()
    if current_state == STATE_TIMING_COMPLETE:
        timing_complete()
    if current_state == RED_ALERT:
        red_alert()

Settings and Customizations

The timer defaults to a 120 minute timer when facing up (LIS3DH on top), and 15 minutes when facing down (QT Py on top). Both timers begin green and fade to red, before entering Red Alert and beginning to blink.

If you're happy with the project as-is, you don't need to make any changes. If you'd like to customize it, you have the following options.

  • activity_one - The length of time in minutes for the first activity. Defaults to 120 minutes (2 hours).
  • activity_two - The length of time in minutes for the second activity. Defaults to 15 minutes.
  • red_alert_delay - The length of time in minutes the timer shows the final color solid before blinking to remind you to move to the next activity. Defaults to 2 minutes. If you flip the the timer within this time, the blinking will not start.
  • color_begin - The color the LEDs begin when the timer begins. Timer fades from color_begin to color_complete to indicate time passing.
  • color_complete - The color the LEDs end when the timer ends. This is also the color that blinks during Red Alert. Timer fades from color_begin to color_complete to indicate time passing.
  • led_brightness - The brightness of the LEDs. If you add a diffuser to the project, you may want to make them brighter. Must be a number between 0.0 and 1.0 where 0.0 is 0% (off), and 1.0 is 100% brightness, e.g. 0.3 would be 30% brightness.
  • red_alert_blink_speed - This is the speed in seconds of the blinking during Red Alert. Defaults to blinking on and off every 0.5 seconds. If you'd like it to be faster, decrease this number. To slow it down, increase the number.

The z-axis values are used to determine the orientation of the timer. The thresholds used in the code assume the LIS3DH is mounted horizontally within the timer, and that the timer is sitting flat. As it doesn't weigh much, the USB cable can sometimes pull it on an angle. If you find the timer isn't running when you want it to because you can't get the timer oriented properly, you can alter these values based on the z-axis values you're getting.

  • down_threshold - The z-axis value must be less than this for the timer to start when in the down orientation. Defaults to -8.
  • up_threshold - The z-axis value must be greater than this for the timer to start when in the up orientation. Defaults to 8.

If you used the parts suggested in the guide, you do not need to change the following. But, in the event you're using a different form of NeoPixels, you can update the following:

  • number_of_pixels - The total number of LEDs connected to your QT Py.

Using the Activity Timer

Plug it in to begin. It starts in idle mode. Flip it to the side that matches the activity you'd like to begin timing.

The timer will begin. By default it fades from green to red.

If you don't do anything when the timer is up, it will enter Red Alert mode and begin blinking.

Flip it over at any time to start the timer for the next activity.

Flip it on its side to stop both of the timers and turn off the LEDs.

This guide was first published on Oct 22, 2020. It was last updated on Oct 22, 2020.

This page (Activity Timer) was last updated on Apr 13, 2021.

Text editor powered by tinymce.