Skip to content

Commit eb241a0

Browse files
authored
Merge pull request adafruit#1279 from kattni/qt-py-timer
Adding activity timer and hydration reminder code.
2 parents e40a34a + 0bb144a commit eb241a0

File tree

2 files changed

+384
-0
lines changed

2 files changed

+384
-0
lines changed

QT_Py_Timer/QT_Py_Activity_Timer.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import time
2+
import board
3+
import adafruit_lis3dh
4+
import neopixel
5+
6+
# Length of each activity, in minutes.
7+
activity_one = 120
8+
activity_two = 15
9+
10+
# Length of time, in minutes, before timer begins Red Alert blinking.
11+
red_alert_delay = 2
12+
13+
# The color will fade from color_begin to color_complete. This is a gentle indicator of how much
14+
# time has passed. Set to any two colors.
15+
color_begin = (0, 255, 0) # Green. Set to any color to begin your countdown.
16+
color_complete = (255, 0, 0) # Red. Set to any color to end your countdown.
17+
led_brightness = 0.2 # Set to a number between 0.0 and 1.0 to set LED brightness.
18+
19+
# Increase or decrease this to change the speed of the Red Alert blinking in seconds.
20+
red_alert_blink_speed = 0.5
21+
22+
# These are the thresholds the z-axis values must exceed for the orientation to be considered
23+
# "up" or "down". These values are valid if the LIS3DH breakout is mounted flat inside the timer
24+
# assembly. If you want the option to have the timer on an angle while timing, you can calibrate
25+
# these values by uncommenting the print(z) in orientation() to see the z-axis values and update
26+
# the thresholds to match the desired range.
27+
down_threshold = -8
28+
up_threshold = 8
29+
30+
# Number of LEDs. Default is 32 (2 x rings of 16 each). If you use a different form of NeoPixels,
31+
# update this to match the total number of pixels.
32+
number_of_pixels = 32
33+
34+
# Set up accelerometer and LEDs.
35+
lis3dh = adafruit_lis3dh.LIS3DH_I2C(board.I2C())
36+
pixels = neopixel.NeoPixel(board.A3, number_of_pixels, brightness=led_brightness)
37+
pixels.fill(0) # Turn off the LEDs if they're on.
38+
39+
STATE_IDLE = "Idle"
40+
STATE_TIMING = "Timing"
41+
STATE_TIMING_COMPLETE = "Timing complete"
42+
RED_ALERT = "Red Alert"
43+
44+
45+
def minutes_to_seconds(minutes):
46+
"""
47+
Convert minutes to seconds for use with time.monotonic().
48+
49+
:param minutes: The number of minutes. Can be a float, e.g. 0.5 for half a minute.
50+
"""
51+
seconds = minutes * 60
52+
return seconds
53+
54+
55+
def gradient(color_one, color_two, blend_weight):
56+
"""
57+
Blend between two colors with a given ratio.
58+
59+
:param color_one: first color, as an (r,g,b) tuple
60+
:param color_two: second color, as an (r,g,b) tuple
61+
:param blend_weight: Blend weight (ratio) of second color, 0.0 to 1.0
62+
"""
63+
if blend_weight < 0.0:
64+
blend_weight = 0.0
65+
elif blend_weight > 1.0:
66+
blend_weight = 1.0
67+
initial_weight = 1.0 - blend_weight
68+
return (int(color_one[0] * initial_weight + color_two[0] * blend_weight),
69+
int(color_one[1] * initial_weight + color_two[1] * blend_weight),
70+
int(color_one[2] * initial_weight + color_two[2] * blend_weight))
71+
72+
73+
# pylint: disable=global-statement
74+
def set_state(state):
75+
global current_state, state_changed
76+
current_state = state
77+
state_changed = time.monotonic()
78+
print("Current state:", current_state) # Print the current state.
79+
80+
81+
def orientation():
82+
"""Determines orientation based on z-axis values. Thresholds set above."""
83+
_, _, z = lis3dh.acceleration
84+
# print(z) # Uncomment to print the z-axis value. Use to calibrate thresholds if desired.
85+
if z < down_threshold:
86+
return 'down'
87+
if z > up_threshold:
88+
return 'up'
89+
return 'side'
90+
91+
92+
def orientation_debounced():
93+
"""
94+
Debounces the orientation changes. For a new orientation to be registered, the timer must
95+
be in that orientation for the duration of the delay.
96+
"""
97+
delay = 0.2
98+
initial_time = time.monotonic()
99+
initial_orientation = orientation()
100+
while True:
101+
new_orientation = orientation()
102+
if new_orientation != initial_orientation:
103+
initial_time = time.monotonic()
104+
initial_orientation = new_orientation
105+
continue
106+
if time.monotonic() - initial_time > delay:
107+
return new_orientation
108+
109+
110+
def state_from_orientation():
111+
"""Determines the state based on orientation."""
112+
global current_orientation
113+
new_orientation = orientation_debounced()
114+
if new_orientation != current_orientation:
115+
if new_orientation == 'side':
116+
set_state(STATE_IDLE)
117+
current_orientation = orientation_debounced()
118+
return
119+
set_state(STATE_TIMING)
120+
current_orientation = orientation_debounced()
121+
122+
123+
def idle():
124+
"""The idle state."""
125+
pixels.fill(0)
126+
state_from_orientation()
127+
128+
129+
def timing():
130+
"""The timing active state."""
131+
state_from_orientation()
132+
133+
if current_orientation == 'up':
134+
activity_duration = minutes_to_seconds(activity_one)
135+
elif current_orientation == 'down':
136+
activity_duration = minutes_to_seconds(activity_two)
137+
else:
138+
return
139+
140+
elapsed = time.monotonic() - state_changed
141+
142+
if elapsed >= activity_duration:
143+
set_state(STATE_TIMING_COMPLETE)
144+
return
145+
146+
blend = (elapsed / activity_duration)
147+
pixels.fill(gradient(color_begin, color_complete, blend))
148+
149+
150+
def timing_complete():
151+
"""The timing complete state."""
152+
pixels.fill(color_complete)
153+
154+
state_from_orientation()
155+
156+
elapsed = time.monotonic() - state_changed
157+
158+
if elapsed >= minutes_to_seconds(red_alert_delay):
159+
set_state(RED_ALERT)
160+
return
161+
162+
163+
blink_is_on = False
164+
165+
166+
def red_alert():
167+
"""The Red Alert state."""
168+
global blink_is_on
169+
elapsed = time.monotonic() - state_changed
170+
if elapsed >= red_alert_blink_speed:
171+
set_state(RED_ALERT)
172+
blink_is_on = not blink_is_on
173+
if blink_is_on:
174+
pixels.fill(color_complete)
175+
else:
176+
pixels.fill(0)
177+
178+
state_from_orientation()
179+
180+
181+
current_orientation = orientation_debounced() # Get the initial orientation.
182+
state_changed = time.monotonic() # Set an initial timestamp.
183+
current_state = STATE_IDLE # Set initial state to idle.
184+
185+
print("Start state:", current_state) # Print the starting state.
186+
187+
while True:
188+
if current_state == STATE_IDLE:
189+
idle()
190+
if current_state == STATE_TIMING:
191+
timing()
192+
if current_state == STATE_TIMING_COMPLETE:
193+
timing_complete()
194+
if current_state == RED_ALERT:
195+
red_alert()
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import time
2+
import board
3+
import adafruit_lis3dh
4+
import neopixel
5+
6+
# The interval you would like to be reminded to drink water, in minutes.
7+
hydration_reminder = 60
8+
9+
# Length of time, in minutes, before timer begins Red Alert blinking.
10+
red_alert_delay = 2
11+
12+
# The color will fade from color_begin to color_complete. This is a gentle indicator of how much
13+
# time has passed. Set to any two colors.
14+
color_begin = (0, 0, 255) # Blue. Set to any color to begin your countdown.
15+
color_complete = (255, 255, 255) # White. Set to any color to end your countdown.
16+
led_brightness = 0.2 # Set to a number between 0.0 and 1.0 to set LED brightness.
17+
18+
# Increase or decrease this to change the speed of the Red Alert blinking in seconds.
19+
red_alert_blink_speed = 0.5
20+
21+
# These are the thresholds the z-axis values must exceed for the orientation to be considered
22+
# "up" or "down". These values are valid if the LIS3DH breakout is mounted horizontally inside the
23+
# timer assembly. If you want the option to have the timer on an angle while timing, you can
24+
# calibrate these values by uncommenting the print(z) in orientation() to see the z-axis values and
25+
# update the thresholds to match the desired range.
26+
down_threshold = -8
27+
up_threshold = 8
28+
29+
# Number of LEDs. Default is 32 (2 x rings of 16 each). If you use a different form of NeoPixels,
30+
# update this to match the total number of pixels.
31+
number_of_pixels = 32
32+
33+
# Set up accelerometer and LEDs.
34+
lis3dh = adafruit_lis3dh.LIS3DH_I2C(board.I2C())
35+
pixels = neopixel.NeoPixel(board.A3, number_of_pixels, brightness=led_brightness)
36+
pixels.fill(0) # Turn off the LEDs if they're on.
37+
38+
STATE_IDLE = "Idle"
39+
STATE_TIMING = "Timing"
40+
STATE_TIMING_COMPLETE = "Timing complete"
41+
RED_ALERT = "Red Alert"
42+
43+
44+
def minutes_to_seconds(minutes):
45+
"""
46+
Convert minutes to seconds for use with time.monotonic().
47+
48+
:param minutes: The number of minutes. Can be a float, e.g. 0.5 for half a minute.
49+
"""
50+
seconds = minutes * 60
51+
return seconds
52+
53+
54+
def gradient(color_one, color_two, blend_weight):
55+
"""
56+
Blend between two colors with a given ratio.
57+
58+
:param color_one: first color, as an (r,g,b) tuple
59+
:param color_two: second color, as an (r,g,b) tuple
60+
:param blend_weight: Blend weight (ratio) of second color, 0.0 to 1.0
61+
"""
62+
if blend_weight < 0.0:
63+
blend_weight = 0.0
64+
elif blend_weight > 1.0:
65+
blend_weight = 1.0
66+
initial_weight = 1.0 - blend_weight
67+
return (int(color_one[0] * initial_weight + color_two[0] * blend_weight),
68+
int(color_one[1] * initial_weight + color_two[1] * blend_weight),
69+
int(color_one[2] * initial_weight + color_two[2] * blend_weight))
70+
71+
72+
# pylint: disable=global-statement
73+
def set_state(state):
74+
global current_state, state_changed
75+
current_state = state
76+
state_changed = time.monotonic()
77+
print("Current state:", current_state) # Print the current state.
78+
79+
80+
def orientation():
81+
"""Determines orientation based on z-axis values. Thresholds set above."""
82+
_, _, z = lis3dh.acceleration
83+
# print(z) # Uncomment to print the z-axis value. Use to calibrate thresholds if desired.
84+
if z < down_threshold:
85+
return 'down'
86+
if z > up_threshold:
87+
return 'up'
88+
return 'side'
89+
90+
91+
def orientation_debounced():
92+
"""
93+
Debounces the orientation changes. For a new orientation to be registered, the timer must
94+
be in that orientation for the duration of the delay.
95+
"""
96+
delay = 0.2
97+
initial_time = time.monotonic()
98+
initial_orientation = orientation()
99+
while True:
100+
new_orientation = orientation()
101+
if new_orientation != initial_orientation:
102+
initial_time = time.monotonic()
103+
initial_orientation = new_orientation
104+
continue
105+
if time.monotonic() - initial_time > delay:
106+
return new_orientation
107+
108+
109+
def state_from_orientation():
110+
"""Determines the state based on orientation."""
111+
global current_orientation
112+
new_orientation = orientation_debounced()
113+
if new_orientation != current_orientation:
114+
if new_orientation == 'side':
115+
set_state(STATE_IDLE)
116+
current_orientation = orientation_debounced()
117+
return
118+
set_state(STATE_TIMING)
119+
current_orientation = orientation_debounced()
120+
121+
122+
def idle():
123+
"""The idle state."""
124+
pixels.fill(0)
125+
state_from_orientation()
126+
127+
128+
def timing():
129+
"""The timing active state."""
130+
state_from_orientation()
131+
132+
activity_duration = minutes_to_seconds(hydration_reminder)
133+
134+
elapsed = time.monotonic() - state_changed
135+
136+
if elapsed >= activity_duration:
137+
set_state(STATE_TIMING_COMPLETE)
138+
return
139+
140+
blend = (elapsed / activity_duration)
141+
pixels.fill(gradient(color_begin, color_complete, blend))
142+
143+
144+
def timing_complete():
145+
"""The timing complete state."""
146+
pixels.fill(color_complete)
147+
148+
state_from_orientation()
149+
150+
elapsed = time.monotonic() - state_changed
151+
152+
if elapsed >= minutes_to_seconds(red_alert_delay):
153+
set_state(RED_ALERT)
154+
return
155+
156+
157+
blink_is_on = False
158+
159+
160+
def red_alert():
161+
"""The Red Alert state."""
162+
global blink_is_on
163+
elapsed = time.monotonic() - state_changed
164+
if elapsed >= red_alert_blink_speed:
165+
set_state(RED_ALERT)
166+
blink_is_on = not blink_is_on
167+
if blink_is_on:
168+
pixels.fill(color_complete)
169+
else:
170+
pixels.fill(0)
171+
172+
state_from_orientation()
173+
174+
175+
current_orientation = orientation_debounced() # Get the initial orientation.
176+
state_changed = time.monotonic() # Set an initial timestamp.
177+
current_state = STATE_IDLE # Set initial state to idle.
178+
179+
print("Start state:", current_state) # Print the starting state.
180+
181+
while True:
182+
if current_state == STATE_IDLE:
183+
idle()
184+
if current_state == STATE_TIMING:
185+
timing()
186+
if current_state == STATE_TIMING_COMPLETE:
187+
timing_complete()
188+
if current_state == RED_ALERT:
189+
red_alert()

0 commit comments

Comments
 (0)