Skip to content

Commit d529158

Browse files
authored
Merge pull request #2507 from kattni/canary-nightlight
Updated for customisability, readability.
2 parents f7e8bbe + 11e3fe8 commit d529158

File tree

1 file changed

+132
-93
lines changed

1 file changed

+132
-93
lines changed

Canary_Nightlight/code.py

Lines changed: 132 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
"""
55
CircuitPython Canary Day and Night Light with Optional Network-Down Detection
66
7-
This project uses the QT Py ESP32-S3 with the NeoPixel 5x5 LED Grid BFF in a 3D printed bird.
8-
The LEDs light up blue or red based on a user-definable time.
7+
This project uses the QT Py ESP32-S3 with the NeoPixel 5x5 LED Grid BFF, along with
8+
a 3D printed bird. The LEDs light up different colors based on the time.
99
1010
In the event that the internet connection fails, it will begin blinking red to notify you.
1111
If the initial test ping fails, and the subsequent pings fail over 30 times, the board
1212
will reset. Otherwise, the blinking will continue until the connection is back up. This
13-
feature is enabled by default. It can easily be disabled at the beginning of the code,
14-
if desired.
13+
feature is enabled by default. It can easily be disabled at the beginning of the code.
1514
"""
1615
import os
1716
import ssl
@@ -26,49 +25,90 @@
2625
import neopixel
2726
from adafruit_io.adafruit_io import IO_HTTP
2827

29-
# This determines whether to run the network-down detection code, and therefore
30-
# whether to run the code that blinks when the network is down.
31-
# Defaults to True. Set to False to disable.
28+
# ============ CUSTOMISATIONS ============
29+
# Network-down detection enable or disable.
30+
# By default, the network-down detection code, and the code that blinks when the
31+
# network is down, are both enabled. If you wish to disable this feature,
32+
# including the blinking, update this to False.
3233
NETWORK_DOWN_DETECTION = True
3334

34-
# This is the number of times ping should fail before it begins blinking.
35-
# If the blinking is happening too often, or if the network is often flaky,
36-
# this value can be increased to extend the number of failures it takes to
37-
# begin blinking. Defaults to 10.
38-
PING_FAIL_NUMBER_TO_BLINK = 10
35+
# The basic canary colors.
36+
# Red light at night is more conducive to sleep. Blue light in the morning is more
37+
# conducive to waking up. The sleep color defaults to red to promote sleep. The wake
38+
# color defaults to blue to promote wakefulness.
39+
SLEEP_COLOR = (255, 0, 0) # Red
40+
WAKE_COLOR = (0, 0, 255) # Blue
3941

40-
# Red light at night is more conducive to sleep. This light is designed
41-
# to turn red at the chosen time to not disrupt sleep.
42-
# This is the hour in 24-hour time at which the light should change to red.
42+
# The blink color.
43+
# This is the color that the canary will blink to notify you that the network is down.
44+
# Defaults to red.
45+
BLINK_COLOR = (255, 0, 0)
46+
47+
# Canary brightness customisation.
48+
# Both the options below must be a float between 0.0 and 1.0, where 0.0 is off, and 1.0 is max.
49+
# This is the brightness of the canary during sleep time. It defaults to 0.2, or "20%".
50+
# Increase or decrease this value to change the brightness.
51+
SLEEP_BRIGHTNESS = 0.2
52+
# This is the brightness of the canary during wake time. It defaults to 0.7, or "70%".
53+
# Increase or decrease this value to change the brightness.
54+
WAKE_BRIGHTNESS = 0.7
55+
56+
# Consecutive ping fail to blink.
57+
# This value is the number of times ping will consecutively fail before the canary begins blinking.
58+
# If the blinking is happening too often, or if the network is often flaky, this value can be
59+
# increased to extend the number of failures it takes to begin blinking.
60+
# Defaults to 10. Must be an integer greater than 1.
61+
CONSECUTIVE_PING_FAIL_TO_BLINK = 10
62+
63+
# Ping interval while ping is successful.
64+
# This is the interval at which the code will send a ping while the network is up and the pings
65+
# are successful. If for any reason you would prefer to slow down the ping interval, this value
66+
# can be updated. Defaults to 1 second. Must be a float greater than 1. Increase this value to
67+
# increase the ping interval time. Do not decrease this value!
68+
UP_PING_INTERVAL = 1
69+
70+
# Checks whether the successful ping interval is below the minimum value.
71+
if UP_PING_INTERVAL < 1:
72+
# If is below the minimum, raise this error and stop the code.
73+
raise ValueError("UP_PING_INTERVAL must be a float greater than 1!")
74+
75+
# Sleep time.
76+
# This is the hour in 24-hour time at which the light should change to the
77+
# desired color for the time you intend to sleep.
4378
# Must be an integer between 0 and 23. Defaults to 20 (8pm).
44-
RED_TIME = 20
79+
SLEEP_TIME = 20
4580

46-
# Blue light in the morning is more conducive to waking up. This light is designed
47-
# to turn blue at the chosen time to promote wakefulness.
48-
# This is the hour in 24-hour time at which the light should change to blue.
81+
# Wake time.
82+
# This is the hour in 24-hour time at which the light should change to the
83+
# desired color for the time you intend to be awake.
4984
# Must be an integer between 0 and 23. Defaults to 6 (6am).
50-
BLUE_TIME = 6
85+
WAKE_TIME = 6
5186

52-
# NeoPixel brightness configuration.
53-
# Both the options below must be a float between 0.0 and 1.0, where 0.0 is off, and 1.0 is max.
54-
# This is the brightness of the LEDs when they are red. As this is expected to be
55-
# during a time when you are heading to sleep, it defaults to 0.2, or "20%".
56-
# Increase or decrease this value to change the brightness.
57-
RED_BRIGHTNESS = 0.2
87+
# Time check interval.
88+
# This sets the time interval at which the code checks Adafruit IO for the current time.
89+
# This is included because Adafruit IO has rate limiting. It ensures that you do not
90+
# hit the rate limit, and the time check does not get throttled.
91+
# Defaults to 300 seconds (5 minutes). Must be a float greater than 300. Increase
92+
# this value to increase the time check interval. Do not decrease this value!
93+
TIME_CHECK_INTERVAL = 300
5894

59-
# This is the brightness of the LEDs when they are blue. As this is expected to be
60-
# during a time when you want wakefulness, it defaults to 0.7, or "70%".
61-
# Increase or decrease this value to change the brightness.
62-
BLUE_BRIGHTNESS = 0.7
95+
# Checks whether the time check interval is below the minimum value.
96+
if TIME_CHECK_INTERVAL < 300:
97+
# If is below the minimum, raise this error and stop the code.
98+
raise ValueError("TIME_CHECK_INTERVAL must be a float greater than 300!")
6399

64-
# Define the light colors. The default colors are blue and red.
65-
BLUE = (0, 0, 255)
66-
RED = (255, 0, 0)
100+
# IP address.
101+
# This is the IP address used to ping to verify that network connectivity is still present.
102+
# To switch to a different IP, update the following. Must be a valid IPV4 address as a
103+
# string (in quotes). Defaults to one of the OpenDNS IPs.
104+
PING_IP = "208.67.222.222"
67105

106+
# ============ HARDWARE AND CODE SET UP ============
68107
# Instantiate the NeoPixel object.
69108
pixels = neopixel.NeoPixel(board.A3, 25)
70109

71110

111+
# Create helper functions
72112
def reload_on_error(delay, error_content=None, reload_type="reload"):
73113
"""
74114
Reset the board when an error is encountered.
@@ -102,17 +142,17 @@ def color_time(current_hour):
102142
:param current_hour: Provide a time, hour only. The `tm_hour` part of the
103143
`io.receive_time()` object is acceptable here.
104144
"""
105-
if BLUE_TIME < RED_TIME:
106-
if BLUE_TIME <= current_hour < RED_TIME:
107-
pixels.brightness = BLUE_BRIGHTNESS
108-
return BLUE
109-
pixels.brightness = RED_BRIGHTNESS
110-
return RED
111-
if RED_TIME <= current_hour < BLUE_TIME:
112-
pixels.brightness = RED_BRIGHTNESS
113-
return RED
114-
pixels.brightness = BLUE_BRIGHTNESS
115-
return BLUE
145+
if WAKE_TIME < SLEEP_TIME:
146+
if WAKE_TIME <= current_hour < SLEEP_TIME:
147+
pixels.brightness = WAKE_BRIGHTNESS
148+
return WAKE_COLOR
149+
pixels.brightness = SLEEP_BRIGHTNESS
150+
return SLEEP_COLOR
151+
if SLEEP_TIME <= current_hour < WAKE_TIME:
152+
pixels.brightness = SLEEP_BRIGHTNESS
153+
return SLEEP_COLOR
154+
pixels.brightness = WAKE_BRIGHTNESS
155+
return WAKE_COLOR
116156

117157

118158
def blink(color):
@@ -121,10 +161,10 @@ def blink(color):
121161
122162
:param tuple color: The color the LEDs will blink.
123163
"""
124-
if color_time(sundial.tm_hour) == RED:
125-
pixels.brightness = RED_BRIGHTNESS
164+
if color_time(sundial.tm_hour) == SLEEP_COLOR:
165+
pixels.brightness = SLEEP_BRIGHTNESS
126166
else:
127-
pixels.brightness = BLUE_BRIGHTNESS
167+
pixels.brightness = WAKE_BRIGHTNESS
128168
pixels.fill(color)
129169
time.sleep(0.5)
130170
pixels.fill((0, 0, 0))
@@ -138,20 +178,23 @@ def blink(color):
138178
pool = socketpool.SocketPool(wifi.radio)
139179
requests = adafruit_requests.Session(pool, ssl.create_default_context())
140180
except Exception as error: # pylint: disable=broad-except
141-
# The exceptions raised by the wifi module are not always clear. If you're receiving errors,
181+
# The exceptions raised by the `wifi` module are not always clear. If you're receiving errors,
142182
# check your SSID and password before continuing.
143183
print("Wifi connection failed.")
144184
reload_on_error(5, error)
145185

146-
# Set up IP address to ping to test internet connection, and do an initial ping.
147-
# This address is an OpenDNS IP.
148-
ip_address = ipaddress.IPv4Address("208.67.222.222")
186+
# Set up IP address to use for pinging, as defined above.
187+
ip_address = ipaddress.IPv4Address(PING_IP)
188+
# Set up ping, and send initial ping.
149189
wifi_ping = wifi.radio.ping(ip=ip_address)
150-
if wifi_ping is None: # If the initial ping is unsuccessful...
151-
print("Setup test-ping failed.") # ...print this message...
152-
initial_ping = False # ...and set initial_ping to False to indicate the failure.
153-
else: # Otherwise...
154-
initial_ping = True # ...set initial_ping to True to indicate success.
190+
# If the initial ping is unsuccessful, print the message.
191+
if wifi_ping is None:
192+
print("Setup test-ping failed.")
193+
# Set `initial_ping` to False to indicate the failure.
194+
initial_ping = False
195+
else:
196+
# Otherwise, set `initial_ping` to True to indicate success.
197+
initial_ping = True
155198

156199
# Set up Adafruit IO. This will provide the current time through `io.receive_time()`.
157200
io = IO_HTTP(os.getenv("aio_username"), os.getenv("aio_key"), requests)
@@ -162,45 +205,44 @@ def blink(color):
162205
try:
163206
sundial = io.receive_time() # Create the sundial variable to keep the time.
164207
except Exception as error: # pylint: disable=broad-except
165-
# If the time retrieval fails with an error...
166-
print(
167-
"Adafruit IO set up and/or time retrieval failed."
168-
) # ...print this message...
169-
reload_on_error(5, error) # ...wait 5 seconds, and soft reload the board.
170-
171-
# Set up various time intervals for tracking non-blocking time intervals
172-
time_check_interval = 300
173-
ping_interval = 1
208+
# If the time retrieval fails with an error, print the message.
209+
print("Adafruit IO set up and/or time retrieval failed.")
210+
# Then wait 5 seconds, and soft reload the board.
211+
reload_on_error(5, error)
174212

175-
# Initialise various time tracking variables
213+
# Initialise various time tracking variables.
176214
ping_time = 0
177215
check_time = 0
178216
ping_fail_time = time.monotonic()
179217

180-
# Initialise ping fail count tracking
218+
# Initialise ping fail count tracking.
181219
ping_fail_count = 0
220+
221+
# ============ LOOP ============
182222
while True:
183223
# Resets current_time to the current time.monotonic() value every time through the loop.
184224
current_time = time.monotonic()
185225
# WiFi and IO connections can fail arbitrarily. The bulk of the loop is included in a
186226
# try/except block to ensure the project will continue to run unattended if any
187227
# failures do occur.
188228
try:
189-
# If the first run of the code, or ping_interval time has passed...
190-
if not ping_time or current_time - ping_time > ping_interval:
229+
# If this is the first run of the code or `UP_PING_INTERVAL` time has passed, continue.
230+
if not ping_time or current_time - ping_time > UP_PING_INTERVAL:
191231
ping_time = time.monotonic()
192-
wifi_ping = wifi.radio.ping(ip=ip_address) # ...ping to verify network connection.
193-
if wifi_ping is not None: # If the ping is successful...
194-
# ...print IP address and ping time.
232+
# Ping to verify network connection.
233+
wifi_ping = wifi.radio.ping(ip=ip_address)
234+
if wifi_ping is not None:
235+
# If the ping is successful, print IP address and ping time.
195236
print(f"Pinging {ip_address}: {wifi_ping} ms")
196237

197-
# If the ping is successful...
238+
# If the ping is successful, continue with this code.
198239
if wifi_ping is not None:
199240
ping_fail_count = 0
200-
# If the first run of the code or time_check_interval has passed...
201-
if not check_time or current_time - check_time > time_check_interval:
241+
# If this is the first run of the code or `TIME_CHECK_INTERVAL` has passed, continue.
242+
if not check_time or current_time - check_time > TIME_CHECK_INTERVAL:
202243
check_time = time.monotonic()
203-
sundial = io.receive_time() # Retrieve the time and save it to sundial.
244+
# Retrieve the time and save it to sundial.
245+
sundial = io.receive_time()
204246
# Print the current date and time to the serial console.
205247
print(f"LED color time-check. Date and time: {sundial.tm_year}-{sundial.tm_mon}-" +
206248
f"{sundial.tm_mday} {sundial.tm_hour}:{sundial.tm_min:02}")
@@ -209,32 +251,29 @@ def blink(color):
209251
# to `fill()` to set the LED color.
210252
pixels.fill(color_time(sundial.tm_hour))
211253

212-
if wifi_ping is None and current_time - ping_fail_time > ping_interval:
213-
# If the ping has failed, and it's been one second (the same interval as the ping)...
214-
ping_fail_time = time.monotonic() # Reset the ping fail time to continue tracking.
215-
ping_fail_count += 1 # Increase the fail count by one.
254+
# If the ping has failed, and it's been one second, continue with this code.
255+
if wifi_ping is None and current_time - ping_fail_time > 1:
256+
ping_fail_time = (time.monotonic()) # Reset the ping fail time to continue tracking.
257+
ping_fail_count += 1 # Add one to the fail count tracking.
216258
print(f"Ping failed {ping_fail_count} times")
217259
# If network down detection is enabled, run the following code.
218260
if NETWORK_DOWN_DETECTION:
219-
# If the ping fail count exceeds the number configured above...
220-
if ping_fail_count > PING_FAIL_NUMBER_TO_BLINK:
221-
blink(RED) # ...begin blinking the LEDs red to indicate the network is down.
261+
# If the ping fail count exceeds the value defined above, begin blinking the LED
262+
# to indicate that the network is down.
263+
if ping_fail_count > CONSECUTIVE_PING_FAIL_TO_BLINK:
264+
blink(BLINK_COLOR)
222265
# If the initial setup ping failed, it means the network connection was failing
223266
# from the beginning, and might require reloading the board. So, if the initial
224-
# ping failed and the ping_fail_count is greater than 30...
267+
# ping failed and the ping_fail_count is greater than 30, immediately soft reload
268+
# the board.
225269
if not initial_ping and ping_fail_count > 30:
226-
reload_on_error(0) # ...immediately soft reload the board.
270+
reload_on_error(0)
227271
# If the initial ping succeeded, the blinking will continue until the connection
228272
# is reestablished and the pings are once again successful.
229273

230-
# There is rarely an issue with pinging which causes the board to fail with a MemoryError.
231-
# The MemoryError is not necessarily related to the code, and the code continues to run
232-
# when this error is ignored. So, in this case, it catches this error separately...
233-
except MemoryError as error:
234-
pass # ...ignores it, and tells the code to continue running.
274+
# ============ ERROR HANDLING ============
235275
# Since network-related code can fail arbitrarily in a variety of ways, this final block is
236-
# included to reset the board when an error (other than the MemoryError) is encountered.
237-
# When the error is thrown...
276+
# included to reset the board when an error is encountered.
277+
# When the error is thrown, wait 10 seconds, and hard reset the board.
238278
except Exception as error: # pylint: disable=broad-except
239-
# ...wait 10 seconds and hard reset the board.
240279
reload_on_error(10, error, reload_type="reset")

0 commit comments

Comments
 (0)